# TOAST Crew State Protocol

Status: implementation contract for the v1 Crew State backend.

This document replaces the old TOAST `KEY:` and `KEY_REQUEST` protocol. It
does not replace LibKeystone messages: LibKS remains an external observation
adapter that may populate the same local cache.

## 1. Goals and boundaries

Crew State distributes volatile, in-game observations:

- owned keystones;
- current Mythic+ score for the logged-in character;
- addon and protocol capability;
- recent cache observations, when a directed cache transfer is needed.

It does not distribute bot exports. `members.lua`, `leaderboard.lua`,
`trends.lua`, benchmarks, and forecasts remain local bot-generated baseline
data. In particular, the protocol must never transfer a full `leaderboard.lua`.

There is no addon-message channel for a WoW Community. `GUILD` is the shared
TIDE transport. Community members outside TIDE communicate only through
targeted `WHISPER` messages with a TIDE broker or collector.

Automatic external `FETCH_REQUEST`s are sent only to a character that is both
currently online in `C_Club` and part of a local leaderboard crew: either the
listed character itself or a `/crew` alt sharing its Discord ID in
`members.lua`. Other known crew members may still request or contribute a
package; TOAST just does not spend proactive fetch traffic on characters it
cannot show.

`C_Club` remains authoritative for presence. Addon traffic is a positive
activity observation (`TOASTERS seen`), never a replacement for C_Club online
presence.

## 2. Transport constraints

- Prefix: `TOAST` (maximum 16 characters).
- Every `C_ChatInfo.SendAddonMessage` message payload is limited to 255
  characters on every channel, including both `GUILD` and `WHISPER`.
- All messages for the `TOAST` prefix share the same server throttle budget,
  regardless of channel. The backend owns one outbound queue for this prefix.
- Protocol payloads use printable ASCII only and target a maximum of 210
  characters per transmitted message.
- A successful send API result means only that WoW queued the message. It is
  not delivery confirmation; acknowledgements are required for transfers.

## 3. Identity and membership contract

Every accepted sender must resolve to exactly one `members.lua` character.

- Primary identity: normalized `character-realm`.
- A name-only sender is accepted only when that name maps unambiguously to one
  roster character.
- Unknown senders, ambiguous names, malformed identities, and records for
  characters absent from `members.lua` are discarded.
- `TIDE` and `CPT` bot-roster prefixes identify *potential* broker candidates.
  A candidate may be an alt that is currently outside the Tide guild, so a
  prefix is never transport proof. A client may use `GUILD` only when its own
  roster identity is `TIDE` or `CPT`, it is in a guild, and
  `GetGuildInfo("player")` resolves to `Tide` (case-insensitive). Only that
  local check allows an ACK, cache sync, claim, relay, or guild broadcast.
  An online prefix candidate that fails the check simply does not ACK and the
  requester tries the next candidate by whisper. `SMN` does not imply TIDE
  guild membership.
- Discord IDs are used for existing alt-aware UI grouping only. They are not a
  prerequisite for protocol membership, broker selection, or identity.
- Blizzard `discordInfo` is optional and is not used as a protocol dependency.

For package records, character and realm are percent-encoded ASCII fields with
`~` as their separator. This is called an `identity` in the wire format.

## 4. Source and trust model

Each stored observation has a source label:

| Source | Meaning |
| --- | --- |
| `SELF` | Read locally for the logged-in character. |
| `TOAST_DIRECT` | Received in that exact character's `SELF_STATE`. |
| `LIBKS_DIRECT` | Received directly from a known character through LibKS. |
| `RELAYED` | Received inside a cache package or guild relay. |
| `BOT` | Local generated baseline from a bot export. |

Rules:

- `SELF` is always the strongest live score source for the current season.
- A recent, valid `RELAYED` current-season peer score may replace an older bot
  score in the score resolver.
- Peer scores never replace a known current-season peer score with a lower
  value.
- A known season mismatch rejects a peer score entirely.
- A key can change level, map, or become explicitly absent; key state is not
  monotonic.
- Received observations use local receipt time. A package can communicate an
  observation age, but no sender timestamp is trusted as authoritative.

## 5. Cache lifecycle and freshness

| Data | Fresh | Retention | Reset / expiry |
| --- | --- | --- | --- |
| Key observation | Under 60 minutes | Current weekly cycle | Clear at EU weekly reset; explicit no-key is valid state. |
| Peer score | Under 24 hours | Current M+ season | Reject/reset on known season change. |
| C_Club presence | Current readable scan | Session + positive history | Never infer offline from a failed scan. |
| TOASTERS seen | Under 60 minutes | Local history | Activity only, not online status. |
| Capability | Valid response within 7 days | Local history | Expire after 7 days. |
| Quiet candidate | No handshake response | Temporary | Suppress for 5 minutes. |
| Full TIDE cache sync | Under 6 hours | Local cache | Request again when stale, new-week, or empty. |

The backend records `lastCacheSyncAttemptAt`, `lastCacheSyncSuccessAt`, and
the current `keyCacheWeekId` independently. A partial transfer never marks a
full cache sync successful.

## 6. Wire format

All new protocol messages start with `CS1`:

```text
CS1|<TYPE>|<fields...>
```

Unknown protocol versions or unknown message types are ignored safely.

### Small messages

| Type | Format | Channel | Meaning |
| --- | --- | --- | --- |
| `H` | `CS1|H|<addonVersion>` | GUILD or WHISPER | Capability hello. |
| `S` | `CS1|S|<requestId>|<addonVersion>|<season>|<score>|<mapId>|<keyLevel>` | GUILD or WHISPER | Own current character state. `requestId` is `-` for proactive state. `0|0` is an explicit no-key. |
| `Q` | `CS1|Q|<requestId>|<reason>` | GUILD | Request online TIDE `SELF_STATE` responses. |
| `Y` | `CS1|Y|<requestId>|<cacheWeekId>` | GUILD | Request one directed full TIDE cache snapshot. |
| `C` | `CS1|C|<requestId>|<claimKind>|<targetIdentity>` | GUILD | Cache-sync or external-fetch claim. |
| `B` | `CS1|B|<requestId>|<addonVersion>` | WHISPER | Outside-TIDE bridge request. |
| `A` | `CS1|A|<requestId>|<role>|<addonVersion>` | WHISPER | Acknowledge bridge, package, or snapshot. |
| `F` | `CS1|F|<requestId>|<targetIdentity>` | WHISPER | TIDE collector asks an external community member for a cache package. |
| `X` | `CS1|X|<requestId>|<reason>` | GUILD or WHISPER | Explicit failure / release of a claim. |
| `U` | `CS1|U|<policyId>|<minimumVersion>|1` | GUILD or WHISPER | TIDE/CPT hard-update policy announcement. Two distinct confirmations are required before an older client blocks normal use. |

`requestId` is a short ASCII nonce generated by the requester. It scopes one
handshake or transfer and prevents delayed chunks from being merged into a new
transfer.

### Chunked transfers

Large transfers use a begin/chunk/end sequence:

```text
CS1|M|<kind>|<requestId>|<chunkCount>
CS1|D|<kind>|<requestId>|<chunkIndex>|<recordList>
CS1|E|<kind>|<requestId>|<recordCount>
CS1|A|<requestId>|RECEIVED|<addonVersion>
```

Kinds:

| Kind | Direction | Purpose |
| --- | --- | --- |
| `CACHE` | WHISPER | Directed `CACHE_PACKAGE` from a requester or fetched external member to a TIDE broker/collector. |
| `SNAP` | WHISPER | Directed `CACHE_SNAPSHOT` from a TIDE cache provider or broker to a requester. |
| `RELAY` | GUILD | Newer external observations only, relayed once to online TIDE clients. |

The receiver accepts a transfer only after a valid matching begin, every chunk
from `1..chunkCount`, and a valid end. Chunks are deduplicated by
`sender + requestId + kind + chunkIndex`. Incomplete transfers expire without
updating `lastCacheSyncSuccessAt`.

Record list syntax uses semicolon-separated records:

```text
K,<identity>,<mapId>,<keyLevel>,<ageSeconds>
R,<identity>,<season>,<score>,<ageSeconds>
```

`ageSeconds` is bounded and converted into a local approximate observation time
at receive time. A record matching the package sender's own identity is stored
as `TOAST_DIRECT`; every other package record is stored as `RELAYED`.

## 7. Normal TIDE behavior

### Proactive own state

The logged-in character broadcasts `SELF_STATE` through `GUILD`:

- after login, with an 8–20 second jitter;
- after a detected owned-key change;
- after a dungeon completion, once key and score APIs are safely readable.

No full cache is broadcast through GUILD.

### Guild state refresh

`STATE_REQUEST` asks online TIDE addons for only `SELF_STATE`. It is used:

- after weekly reset;
- when a broker must serve a requester and the last guild sweep is over 2
  minutes old;
- as a low-rate self-healing refresh (target interval: 20 minutes).

Responses use 0–3 seconds of jitter and are cancelled if an equivalent request
is observed first.

### Returning TIDE cache sync

A TIDE player sends `CACHE_SYNC_REQUEST` only when not in combat and when the
cache is empty, belongs to an old week, has never successfully synced, or is
older than 6 hours. The request is delayed by login jitter.

Eligible TIDE providers claim the request during a short claim window. The
deterministic winner sends one `SNAP` transfer by whisper. The requester sends
an acknowledgement after a complete merge.

## 8. Outside-TIDE bridge and community harvesting

### Outside-TIDE requester

An online community member outside TIDE chooses only an online, exact
bot-roster `TIDE` or `CPT` character as broker target. It sends `BRIDGE_REQUEST`
and waits for an acknowledgement.

On acknowledgement:

1. requester sends `CACHE_PACKAGE` to the broker;
2. broker merges it and optionally performs a fresh guild state sweep;
3. broker returns a `CACHE_SNAPSHOT`;
4. requester acknowledges the completed snapshot.

No outside-TIDE to outside-TIDE whisper mesh is created.

### TIDE community harvesting

TIDE clients may discover online external community roster characters through
C_Club. Before whispering one, a TIDE client publishes `FETCH_CLAIM` in GUILD.
After the claim window, one deterministic winner sends `FETCH_REQUEST`.

The external member returns `CACHE_PACKAGE`. The collector sends only newly
accepted records in a `RELAY` transfer through GUILD. Recipients merge these
records but never relay them through GUILD again.

## 9. Handshake failure and backoff

No acknowledgement is not proof that the target lacks TOAST. It can mean combat,
loading, a missed whisper, a disabled addon, or a server throttle.

For a broker request:

1. try each current eligible online candidate once;
2. if none acknowledge, retry the candidate round after 5 minutes;
3. then after 15 minutes;
4. then after 30 minutes;
5. stop automatic retries until a new trigger: manual refresh, login, relevant
   dashboard open, or newly detected eligible TIDE presence.

If no eligible broker is visible through C_Club, send no whisper and do not
count a failed round. `TargetOffline` skips a candidate immediately.
`AddOnMessageLockdown` or local combat defers the action until safe.

## 10. Queue priorities

One queue owns all `TOAST` sends. Higher priority messages are sent first:

1. acknowledgements, failures, and claim control;
2. own `SELF_STATE` after a meaningful local change;
3. active broker / cache-sync snapshot chunks;
4. `STATE_REQUEST` responses;
5. `RELAY` chunks and community harvesting.

The queue checks `C_ChatInfo.SendAddonMessage` results. Throttle failures are
requeued with backoff; target-offline results terminate the targeted action.

## 11. LibKS adapter

LibKS is not a TOAST protocol peer. A valid LibKS key/rating message from a
known sender is merged as `LIBKS_DIRECT` and can appear in a later directed
cache snapshot. TOAST does not send LibKS requests or broadcast TOAST state on
the LibKS prefix.

## 12. Test strategy

The parser, validator, package assembler, merge engine, candidate selection,
and queue intent builder must be pure Lua helpers wherever possible. A local
debug harness can inject raw messages and inspect resulting cache/intent state
without requiring multiple WoW accounts.

Real in-game beta tests then focus on a small number of end-to-end cases:

- two simultaneous claims;
- returning TIDE cache sync;
- outside-TIDE broker success and no-ack backoff;
- external community harvest and one-hop relay;
- combat deferral;
- malformed / incomplete chunks;
- weekly reset and season change.

## 13. Local debug screen

`/toast debug` opens a local-only Crew State diagnostics window. It displays
the selected role, queue depth, bridge state, transfer/claim counts, key-cache
week, and cache-sync timestamps. Opening it sends nothing. Its **Refresh
actions** button invokes the normal, cooldown-governed refresh path, so it is
useful during testing without creating a special debug protocol.
