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

# Cross-Platform Sessions

> One conversation across Telegram, Discord, Slack — unified per-user history with opt-in identity linking

Cross-platform sessions let one user keep a single conversation across every messaging platform.

<Tabs>
  <Tab title="Recommended (with pairing)">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import BotOS, StoreBackedIdentityResolver
    from praisonaiagents import Agent

    agent = Agent(name="assistant", instructions="Be helpful.")

    # Reuses ~/.praisonai/identity.json + the gateway pairing store.
    resolver = StoreBackedIdentityResolver.from_env()

    BotOS(
        agent=agent,
        platforms=["telegram", "whatsapp", "discord", "slack"],
        identity_resolver=resolver,
    ).run()
    ```
  </Tab>

  <Tab title="Explicit links only">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import BotOS
    from praisonaiagents import Agent
    from praisonaiagents.session import FileIdentityResolver

    agent = Agent(name="assistant", instructions="Be helpful.")

    resolver = FileIdentityResolver()                        # ~/.praisonai/identity.json
    resolver.link("telegram", "12345",       "alice")
    resolver.link("discord",  "snowflake-1", "alice")

    BotOS(
        agent=agent,
        platforms=["telegram", "discord"],
        identity_resolver=resolver,
    ).run()
    ```
  </Tab>
</Tabs>

The user continues the same conversation on Telegram, then Discord; identity linking mirrors session history across platforms.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Cross-Platform Mirror"
        T[📱 Telegram: 12345] --> R[🪪 IdentityResolver]
        D[💬 Discord: snowflake-1] --> R
        S[💼 Slack: U-A] --> R
        R --> U[👤 alice]
        U --> H[💾 One Session History]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff

    class T,D,S input
    class R process
    class U,H output

```

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Alice as 👤 Alice
    participant TG as 📱 Telegram
    participant DC as 💬 Discord
    participant Bot as 🤖 BotOS
    participant Hist as 💾 Unified History

    Alice->>TG: "My favourite colour is octarine."
    TG->>Bot: chat(user_id=12345)
    Bot->>Hist: append (key=alice)
    Bot-->>TG: "Got it!"
    Note over Alice,Hist: Hours later, different platform

    Alice->>DC: "What's my favourite colour?"
    DC->>Bot: chat(user_id=snowflake-1)
    Bot->>Hist: load (key=alice — same!)
    Bot-->>DC: "Octarine."
```

## Quick Start

<Steps>
  <Step title="One platform, one user">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import Bot
    from praisonaiagents import Agent

    agent = Agent(name="assistant", instructions="Be helpful.")
    bot = Bot("telegram", agent=agent)
    await bot.start()
    ```
  </Step>

  <Step title="Two platforms, one user (recommended)">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import BotOS, StoreBackedIdentityResolver
    from praisonaiagents import Agent

    agent = Agent(name="assistant", instructions="Be helpful.")

    resolver = StoreBackedIdentityResolver.from_env()

    botos = BotOS(
        agent=agent,
        platforms=["telegram", "discord"],
        identity_resolver=resolver,
    )
    await botos.start()
    ```
  </Step>

  <Step title="In-process testing / ephemeral">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import BotOS
    from praisonaiagents import Agent
    from praisonaiagents.session import InMemoryIdentityResolver

    agent = Agent(name="assistant", instructions="Be helpful.")

    resolver = InMemoryIdentityResolver()  # Ephemeral for tests
    resolver.link("telegram", "12345", "alice")
    resolver.link("discord", "snowflake-1", "alice")

    botos = BotOS(
        agent=agent,
        platforms=["telegram", "discord"],
        identity_resolver=resolver,
    )
    ```
  </Step>
</Steps>

***

## Choosing Your Resolver

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What do you need?} -->|tests / single proc| IM[InMemoryIdentityResolver]
    Q -->|production + paired users| SB[StoreBackedIdentityResolver<br/>⭐ recommended]
    Q -->|production, no pairing| FI[FileIdentityResolver]
    Q -->|multi-process / multi-host| Custom[Custom IdentityResolverProtocol<br/>SQLite / Redis / DB]

    classDef opt fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef star fill:#10B981,stroke:#7C90A0,color:#fff
    class IM,FI,Custom opt
    class SB star
```

***

## StoreBackedIdentityResolver

`StoreBackedIdentityResolver` extends the core `FileIdentityResolver` with a read-through to the gateway pairing store, so users who have already paired share a session out of the box.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[📨 Incoming message<br/>platform + user_id] --> L1{Explicit link?}
    L1 -->|Yes| Out[👤 canonical id]
    L1 -->|No| L2{Paired with<br/>non-empty label?}
    L2 -->|Yes| Out
    L2 -->|No| Fallback[🪪 platform:user_id]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef decide fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff
    classDef neutral fill:#189AB4,stroke:#7C90A0,color:#fff

    class Start input
    class L1,L2 decide
    class Out good
    class Fallback neutral
```

**Resolution order:**

1. **Explicit link** registered via `link()` (highest priority)
2. **Pairing-store label** for `(user_id, platform)` when the channel is paired and carries a non-empty `label`
3. **`f"{platform}:{user_id}"`** — safe per-platform fallback (no merging)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.bots import StoreBackedIdentityResolver

# Defaults: ~/.praisonai/identity.json + gateway pairing store
resolver = StoreBackedIdentityResolver.from_env()

# Override paths
resolver = StoreBackedIdentityResolver.from_env(
    path="/etc/praisonai/identity.json",
    store_dir="/var/lib/praisonai/gateway",
)

# Direct construction with an existing pairing store
from praisonai.gateway.pairing import PairingStore
resolver = StoreBackedIdentityResolver(
    path="~/.praisonai/identity.json",
    pairing_store=PairingStore(),
    use_pairing_label=True,   # default
)
```

### link\_paired() — Promote Paired Users to Explicit Links

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# After channels have been paired with non-empty labels via the pairing CLI:
count = resolver.link_paired()
print(f"Materialised {count} paired channels into explicit links.")
```

Useful when the pairing store is volatile or about to be rotated — this pins the canonical-id mapping into the resolver's own JSON file.

<Tip>
  If you already use `praisonai pairing approve <platform> <code> --label <canonical>`, switching to `StoreBackedIdentityResolver.from_env()` is a one-line upgrade: paired users immediately share one session across all their channels, with no separate `praisonai identity link` calls required. Run `praisonai identity import` once to pin those mappings into the explicit link map.
</Tip>

***

## In the gateway (`praisonai gateway start`)

The flagship `WebSocketGateway` daemon accepts the same resolver, so a paired user keeps one continuous session across every channel served by one gateway process.

Wire it three ways — YAML block, CLI flag, or Python override.

<Tabs>
  <Tab title="YAML (recommended for daemons)">
    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # gateway.yaml
    identity:
      enabled: true                          # default when block is present
      store: ~/.praisonai/identity.json      # optional link-map path (also accepts `path:`)

    agents:
      assistant:
        instructions: "Be helpful."

    channels:
      telegram:  { token: "${TELEGRAM_BOT_TOKEN}" }
      discord:   { token: "${DISCORD_BOT_TOKEN}" }
    ```

    * A missing block, a non-mapping block, or `enabled: false` keeps today's default — per-platform session keys.
    * `enabled` accepts `true`/`false`/`"1"`/`"true"`/`"yes"`/`"on"` (strings coerced case-insensitively).
    * `store` is `~`-expanded; `path` is accepted as an alias.
    * Any failure to build the resolver degrades gracefully to per-platform keys and logs a warning — startup never aborts.
  </Tab>

  <Tab title="CLI (--identity-store)">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai gateway start --config gateway.yaml \
      --identity-store ~/.praisonai/identity.json
    ```

    * The CLI value overrides the YAML `identity:` block.
    * The value is `~`-expanded.
    * If `StoreBackedIdentityResolver` can't be imported (optional deps missing), a warning is logged and the gateway continues without the resolver.
  </Tab>

  <Tab title="Python override (always wins)">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai_bot.gateway.server import WebSocketGateway
    from praisonai_bot.bots import StoreBackedIdentityResolver

    resolver = StoreBackedIdentityResolver.from_env()
    gateway = WebSocketGateway(
        host="127.0.0.1",
        port=8765,
        identity_resolver=resolver,   # #3020 — stamped onto every channel bot
    )
    await gateway.start_with_config("gateway.yaml")
    ```

    A constructor resolver is marked explicit and is **never** rebuilt from YAML, even on hot-reload.
  </Tab>
</Tabs>

### Precedence

`WebSocketGateway(identity_resolver=…) > --identity-store > identity: YAML block > default (per-platform keys)`

An explicit constructor or CLI resolver is never clobbered by the YAML block, even on hot-reload.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{Resolver source?} -->|WebSocketGateway arg| P1[🐍 Python override<br/>always wins]
    Q -->|--identity-store| P2[💻 CLI flag]
    Q -->|identity: block| P3[📄 YAML block]
    Q -->|none| P4[🪪 per-platform keys<br/>bot_platform_userid]

    P1 --> Win[✅ Stamped onto every channel bot]
    P2 --> Win
    P3 --> Win

    classDef decide fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef py fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef cli fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef yaml fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff

    class Q decide
    class P1 py
    class P2 cli
    class P3,P4 yaml
    class Win good
```

The gateway stamps the resolver onto each channel bot's session manager at startup and on every hot-reload:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant GW as 🗼 Gateway
    participant Res as 🪪 IdentityResolver
    participant Ch1 as 📱 Telegram bot
    participant Ch2 as 💬 Discord bot

    GW->>Res: build from YAML / CLI / constructor
    GW->>Ch1: start_channels → _stamp_identity_resolver(bot)
    Ch1->>Res: bot._session._identity_resolver = resolver
    GW->>Ch2: start_channels → _stamp_identity_resolver(bot)
    Ch2->>Res: bot._session._identity_resolver = resolver
    Note over GW,Ch2: Hot-reload restarts a channel → re-stamped in _start_single_channel
```

<Note>
  **Gateway process:** In the `praisonai gateway start` daemon, the resolver is stamped onto each channel bot's session manager via `WebSocketGateway._stamp_identity_resolver()` (mirrors `_stamp_admission_gate`). Restarted channels during hot-reload keep the resolver too — see [Gateway Config Reload](/docs/docs/features/gateway-config-reload#reloadable-sections-identity).
</Note>

### End-to-end: one session across two platforms

1. Operator approves both channels for the same label:
   ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
   praisonai pairing approve telegram <code> --label alice
   praisonai pairing approve discord  <code> --label alice
   ```
2. Operator adds `identity: { enabled: true }` to `gateway.yaml` (or passes `--identity-store`).
3. Alice on Telegram: `"my favourite colour is octarine"`.
4. Hours later, Alice on Discord: `"what's my favourite colour?"`.
5. The gateway resolves both to `alice`, loads the same session history, and replies `"Octarine."`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Alice as 👤 Alice
    participant TG as 📱 Telegram
    participant DC as 💬 Discord
    participant GW as 🗼 Gateway
    participant Hist as 💾 Unified History

    Alice->>TG: "My favourite colour is octarine."
    TG->>GW: chat(user_id=12345)
    GW->>Hist: append (key=alice)
    GW-->>TG: "Got it!"
    Note over Alice,Hist: Hours later, different platform
    Alice->>DC: "What's my favourite colour?"
    DC->>GW: chat(user_id=snowflake-1)
    GW->>Hist: load (key=alice — same!)
    GW-->>DC: "Octarine."
```

***

## CLI: praisonai identity

Four subcommands manage identity links from the command line.

| Command                                                    | Purpose                                                |
| ---------------------------------------------------------- | ------------------------------------------------------ |
| `praisonai identity link <platform> <user_id> <canonical>` | Map a `(platform, user_id)` to a canonical identity    |
| `praisonai identity unlink <platform> <user_id>`           | Remove a mapping                                       |
| `praisonai identity list [canonical]`                      | Show all links, or links for a single canonical id     |
| `praisonai identity import`                                | Materialise paired channels as explicit identity links |

Each subcommand accepts:

| Option        | Default                                                      | Description                          |
| ------------- | ------------------------------------------------------------ | ------------------------------------ |
| `--path`      | `~/.praisonai/identity.json` (or `$PRAISONAI_IDENTITY_PATH`) | Override the link-map JSON path      |
| `--store-dir` | gateway default                                              | Override the pairing store directory |

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Link Alice's Telegram + WhatsApp identities to one canonical id
praisonai identity link telegram 12345 alice
praisonai identity link whatsapp "+44123456789" alice

# Inspect
praisonai identity list alice
# Links for alice:
#   telegram:12345
#   whatsapp:+44123456789

# Or list every mapping
praisonai identity list

# Unlink one channel
praisonai identity unlink telegram 12345

# Promote paired channels (labelled via `praisonai pairing approve ... --label`) into explicit links
praisonai identity import
# ✅ Imported 3 identity link(s) from pairing store
```

***

## SessionContext for Tools

Any tool the agent calls can read who is messaging:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.session import get_session_context

def whoami() -> str:
    ctx = get_session_context()
    return f"You are {ctx.user_name or ctx.user_id} on {ctx.platform}"

agent = Agent(name="assistant", instructions="Use whoami when asked.", tools=[whoami])
```

### SessionContext Fields

| Field               | Type                              | Description                                        |                                                                                                  |
| ------------------- | --------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `platform`          | `str`                             | `"telegram"`, `"discord"`, `"slack"`, …            |                                                                                                  |
| `chat_id`           | `str`                             | Platform chat / channel id                         |                                                                                                  |
| `chat_name`         | `str`                             | Human-readable channel name                        |                                                                                                  |
| `thread_id`         | `str`                             | Thread / topic id (Slack threads, Telegram topics) |                                                                                                  |
| `user_id`           | `str`                             | Raw platform user id                               |                                                                                                  |
| `user_name`         | `str`                             | Display name                                       |                                                                                                  |
| `unified_user_id`   | `str`                             | Result of `IdentityResolver.resolve()`             |                                                                                                  |
| `origin`            | `Optional[Origin]`                | `None`                                             | Platform-aware origin info — see [Platform-Aware Agents](/docs/features/platform-aware-agents)        |
| `reachable_targets` | `Optional[List[ReachableTarget]]` | `None`                                             | Channels the agent can deliver to — see [Platform-Aware Agents](/docs/features/platform-aware-agents) |

<Note>
  Use `set_session_context` / `clear_session_context` for advanced custom adapters. Returns a token; reset in a `finally` block.
</Note>

***

## Concurrent Turns Across Adapters

With an identity resolver configured, near-simultaneous turns from different adapters that resolve to the same unified user run one at a time against that user's single transcript.

Each adapter — Telegram, Discord, Slack — keeps its own session manager, and the identity resolver unifies them onto one persisted session per human. Before this fix, each adapter still guarded its turns separately, so two adapters could run turns for the same human at the same time and scramble the order of their shared transcript.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Alice as 👤 Alice
    participant TG as 📱 Telegram
    participant DC as 💬 Discord
    participant BotOS as 🤖 BotOS
    participant Hist as 💾 One Session

    par Two platforms, same instant
        Alice->>TG: "What's my favourite colour?"
        Alice->>DC: "And my favourite food?"
    end
    TG->>BotOS: chat(user=alice)
    DC->>BotOS: chat(user=alice)
    Note over BotOS: shared turn lock on resolved id
    BotOS->>Hist: Turn 1 append + reply (Telegram)
    BotOS-->>TG: "Octarine."
    BotOS->>Hist: Turn 2 append + reply (Discord)
    BotOS-->>DC: "Pizza."
```

| When                                 | What happens                                                                                                                        |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| One adapter, one user                | Turns already serialise per user — unchanged.                                                                                       |
| Multiple adapters, **no** resolver   | Each adapter keeps its own per-user history (`bot_{platform}_{user_id}`) — no unified transcript, no shared lock needed. Unchanged. |
| Multiple adapters, **with** resolver | Turns from any adapter for the same unified user run serially against the one shared transcript. **This is the new behaviour.**     |

### When it matters

* A cron or scheduled outbound delivery lands on platform A while the user replies on platform B — the mirror and the inbound reply serialise cleanly (see [Mirror for Outbound Deliveries](#mirror-for-outbound-deliveries)).
* A user types on Telegram while their smartwatch fires a Slack message in the same second — the second turn waits for the first to commit before appending, so the transcript stays in strict `user → assistant → user` order.

<Note>
  Nothing to configure. This is automatic whenever an identity resolver is wired via `BotOS(identity_resolver=…)`, the gateway's `identity:` YAML block, or `--identity-store`. Deployments without a resolver are unchanged.
</Note>

<Note>
  **Multi-replica gateways:** the shared turn lock stamped onto channel bots is in-process only. If you scale the gateway to `replicas > 1`, enable a distributed backend — see [Gateway Turn Lock](/docs/features/gateway-turn-lock).
</Note>

***

## Mirror for Outbound Deliveries

Cron jobs, scheduled deliveries, and cross-platform replies need to **mirror** the assistant's outbound message into the user's history:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.bots import mirror_to_session

# After sending a notification programmatically
mirror_to_session(
    session_mgr=bot._session,
    user_id="alice",
    message_text="Reminder: your standup is in 5 minutes.",
    source_label="cron",
)
```

### Parameters

| Parameter      | Type                | Description                                               |
| -------------- | ------------------- | --------------------------------------------------------- |
| `session_mgr`  | `BotSessionManager` | Session manager instance                                  |
| `user_id`      | `str`               | Unified user ID                                           |
| `message_text` | `str`               | Message content to mirror                                 |
| `source_label` | `str`               | Source identifier (`"cron"`, `"web"`, `"cross_platform"`) |
| `metadata`     | `dict`              | Optional extra metadata                                   |
| `lock`         | `threading.RLock`   | Optional lock for synchronization                         |

<Note>
  Errors are swallowed and logged — a mirror failure must never break the outbound delivery itself.
</Note>

***

## Storage & Privacy

<Note>
  `FileIdentityResolver` defaults to `~/.praisonai/identity.json` (override via `PRAISONAI_IDENTITY_PATH` env var or constructor `path=`). File is written atomically and chmod 0o600.
</Note>

<Warning>
  Identity links are **explicit and opt-in**. No automatic linking — wire the resolver only after a verified DM-pairing flow confirms the same human controls both accounts.
</Warning>

<Tip>
  Without a resolver, the legacy `bot_{platform}_{user_id}` storage key is preserved bit-for-bit — fully backward compatible.
</Tip>

<Note>
  Running the flagship daemon? See [In the gateway (`praisonai gateway start`)](#in-the-gateway-praisonai-gateway-start) for wiring the same resolver via the `identity:` YAML block, `--identity-store`, or the `WebSocketGateway` constructor.
</Note>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use StoreBackedIdentityResolver for production (with pairing)">
    For production wrapper deployments, use `StoreBackedIdentityResolver.from_env()` — it picks up paired channels automatically and falls back to the same safe `platform:user_id` default. Plain `FileIdentityResolver` is the right choice only when you do not use the pairing system.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import StoreBackedIdentityResolver

    resolver = StoreBackedIdentityResolver.from_env()
    ```
  </Accordion>

  <Accordion title="Use FileIdentityResolver for explicit-only links">
    For production single-host bots without pairing integration, `FileIdentityResolver` provides persistent storage with atomic writes and proper file permissions.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.session import FileIdentityResolver

    resolver = FileIdentityResolver()  # ~/.praisonai/identity.json
    ```
  </Accordion>

  <Accordion title="Implement custom IdentityResolverProtocol for scale">
    For multi-process/multi-host deployments, back it with SQLite, Redis, or a database:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.session import IdentityResolverProtocol

    class DatabaseIdentityResolver:
        def resolve(self, platform: str, platform_user_id: str) -> str:
            # Query your database
            pass
        
        def link(self, platform: str, platform_user_id: str, unified_user_id: str) -> None:
            # Store in your database
            pass
    ```
  </Accordion>

  <Accordion title="Pair before linking">
    Never auto-link based on display name. Always require a DM-verified pairing flow:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # After DM verification succeeds
    resolver.link("telegram", telegram_user_id, verified_unified_id)
    resolver.link("discord", discord_user_id, verified_unified_id)
    ```
  </Accordion>

  <Accordion title="Read SessionContext in tools">
    Instead of `os.environ`, read `SessionContext` — concurrent message handlers won't trample each other:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    def get_user_platform() -> str:
        ctx = get_session_context()
        return ctx.platform  # Thread-safe, context-aware
    ```
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="BotOS" icon="robot" href="/docs/features/botos">
    Multi-platform bot orchestrator
  </Card>

  <Card title="Messaging Bots" icon="message-circle" href="/docs/features/messaging-bots">
    Platform-specific bot guides
  </Card>

  <Card title="Bot Pairing" icon="handshake" href="/docs/features/bot-pairing">
    Secure unknown-user onboarding
  </Card>

  <Card title="Unknown-User Pairing" icon="user-check" href="/docs/features/bot-unknown-user-pairing">
    Inline-button approval for bots
  </Card>

  <Card title="Gateway CLI" icon="tower-broadcast" href="/docs/features/gateway-cli">
    `--identity-store` and other `gateway start` flags
  </Card>

  <Card title="Gateway Config Reload" icon="arrows-rotate" href="/docs/features/gateway-config-reload">
    Hot-reload the `identity:` block without dropping turns
  </Card>
</CardGroup>
