Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 44 additions & 39 deletions CONNECTIVITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,10 @@ The hub does not create a session. It can only advise and relay:

- **Peer degradation (no auto-cascade)**: When the peer becomes unreachable
(delivery failures, 30-second silence, or hub disconnect) while the
session is off-chain, `peerLiveness` moves to `'degraded'` (yellow dot).
There is no automatic go-on-chain — the user must decide to escalate.
Only explicit terminal signals (user clicks "Go On-Chain" or receives a
FOAD) mark the peer as dead.
session is off-chain, `peerLiveness` moves to `'degraded'` (yellow banner
rail; tab stays a link). There is no automatic go-on-chain — the user must
decide to escalate. Only explicit terminal signals (user clicks "Go
On-Chain" or receives a FOAD) mark the peer as dead.

- **Cascade warning dialogs**: Confirmation dialogs currently warn before
disconnecting or switching hubs when a peer/session would be affected.
Expand All @@ -485,41 +485,48 @@ The hub does not create a session. It can only advise and relay:

## UX: Connectivity Indicators

### Tab dots
### Tab pipe marks

Each tab in the tab bar has a small colored dot to the left of its label
text, indicating the connectivity health of the axis associated with that
tab. The dot is always present (gray when idle/irrelevant) so the tab bar
layout never shifts.
Wallet, Hub, and Game tabs show an uncolored link (connected) or broken-chain
(disconnected) emoji to the left of the label. History and Log have no pipe
mark. The existing upper-right notification dots indicate unread activity and
are unchanged.

Separately, the existing upper-right notification dots indicate unread
activity (new game events, etc.). These are unchanged and serve a different
purpose.
Pipe marks answer only “is this pipe up?” Session mode lives on the game
dashboard banner rail, not on the tabs.

### Per-tab color semantics
| Tab | Link | Broken chain |
|-----|------|----------------|
| Wallet | Connected. | Disconnected. The **Wallet** label is also red. |
| Hub | `hubLiveness === 'connected'` | Reconnecting, inactive, disconnected, or never connected |
| Game | Live session and peer is not `dead` | `sessionPhase` none/resolved, or `peerLiveness === 'dead'` |

| Tab | Green | Yellow | Red | Gray |
|-----|-------|--------|-----|------|
| Wallet | Connected | — | Disconnected | — |
| Hub | Connected | Reconnecting | Inactive (no heartbeat) | Not connected (null / disconnected) |
| Game | Peer connected (incl. clean shutdown) | On-chain, peer degraded, or peer unreachable during clean shutdown | Error, or peer dead outside clean shutdown | No session / resolved |
| History | — | — | — | Always gray |
| Log | — | — | — | Always gray |
Handshake with `peerLiveness === null` counts as connected. `degraded` pings
stay a link; that warning is banner-only. `sessionError` does not affect the
tab mark.

### Game tab dot priority
### Game dashboard banner rail

The Game tab dot checks conditions in this order:
The session dashboard has a full-height left-edge color rail:

1. `sessionPhase === 'none' || 'resolved'` → **gray** (no active session)
2. `sessionError` → **red** (genuine error — always wins)
3. Clean shutdown in progress (`ShuttingDown` / `ShutdownTransactionPending` /
`cleanShutdownStarted`):
- peer degraded (or unexpectedly dead) → **yellow** (unreachable)
- otherwise → **green** (cooperative close in flight; keepalives continue)
4. `peerLiveness === 'dead'` → **red** (terminal — go-on-chain or FOAD)
5. `sessionPhase === 'on-chain'` or `peerLiveness === 'degraded'` → **yellow** (resolving or stale peer)
6. `peerLiveness === 'connected'` → **green** (playing normally)
7. Otherwise → **gray**
| Tone | Color | When |
|------|-------|------|
| `idle` | Gray | No session / never set up |
| `playing` | Green | Setup, handshake, off-chain play, cooperative shutdown |
| `pings-bad` | Yellow | Same as playing, but `peerLiveness === 'degraded'` |
| `on-chain` | Red | Going on-chain, unrolling, or a resolved unroll that still has games |
| `ended` | Blue | Terminal dashboard still showing (clean resolve, failed, abandoned) |

On-chain beats yellow. Failed/stale outcomes that are actually over stay
`ended`; the Channel label still names the outcome. Yellow also shows
“Peer pings look stuck.”

### Game tab connectedness

`selectGameTabConnected` is true unless:

1. `sessionPhase === 'none' || 'resolved'`, or
2. `peerLiveness === 'dead'`

Clean shutdown does **not** mark the peer dead on its own. Keepalives and the
small allowlist of shutdown-related peer messages continue until local
Expand All @@ -528,17 +535,14 @@ shutdown completes. Successful/terminal session exit does not send
does arrive during pre-active matchmaking, it is honored as an abort: cancel
the attempt (including any in-flight async session start), surface
cancelled/error, and do not leave an orphan handshake. When the channel
reaches a terminal state the session exits and the dot goes gray.
reaches a terminal state the session exits and the game tab shows a broken
chain.

### Game tab error conditions (red dot)
### Session error conditions

The Game tab shows a red dot when `sessionError` is true, or when
`peerLiveness === 'dead'` outside a clean shutdown (go-on-chain or FOAD).
`sessionError` is derived from:

- `Failed` channel state — the channel encountered an unrecoverable error
- `ResolvedStale` channel state — the channel resolved but the outcome is
suspect (e.g., opponent exploited a timeout)
- `ResolvedStale` channel state — the channel resolved but the outcome is
suspect (e.g., opponent exploited a timeout)
- `game-error` game terminal — a generic game-level error (`GameStatus` with
Expand All @@ -549,7 +553,8 @@ The Game tab shows a red dot when `sessionError` is true, or when

Normal settlements such as `accept_settlement`, `settled_cleanly`,
`opponent_timed_out`, `we_accepted`, and `slashed_opponent` are **not** session
errors.
errors. These conditions do not change the tab pipe mark; terminal outcomes
use the `ended` banner rail.

### Settlement labels

Expand Down
17 changes: 9 additions & 8 deletions FRONTEND_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -902,8 +902,9 @@ delivery, ack reception, and keepalive reception.

Peer liveness is measured passively from relay traffic. The `PeerSession` object
derives liveness indicators using a 5-second polling interval. These feed into
the **tab-dot connectivity indicators** — colored dots to the left of each tab
label showing connection health (green / yellow / red / gray). They are also
the **tab pipe marks** — uncolored link / broken-chain emojis to the left of
Wallet, Hub, and Game tab labels — and into the game dashboard **banner rail**
(session mode: idle / playing / pings-bad / on-chain / ended). They are also
passed to `GameSession` for in-game display. Separately, Shell has a cascade
rule: if the peer is marked lost while the session is still off-chain, it calls
`goOnChain()` on the WASM cradle.
Expand Down Expand Up @@ -931,12 +932,12 @@ Connected, keepalive timeout while WS is up → Inactive.

**Peer indicator** (`PeerLiveness`) has four states:

| State | Meaning | Dot color |
| ----------- | ----------------------------------------------------------------------------------- | --------- |
| `connected` | Peer traffic received within the last 30 seconds | Green |
| `degraded` | Delivery failure reported by hub, or no peer traffic for 30+ seconds | Yellow |
| `dead` | Local go-on-chain or session rejection (FOAD) — terminal for this peer relationship | Red |
| `null` | No active peer session | Grey |
| State | Meaning | Tab mark |
| ----------- | ----------------------------------------------------------------------------------- | -------- |
| `connected` | Peer traffic received within the last 30 seconds | Link |
| `degraded` | Delivery failure reported by hub, or no peer traffic for 30+ seconds | Link (banner rail yellow) |
| `dead` | Local go-on-chain or session rejection (FOAD) — terminal for this peer relationship | Broken chain |
| `null` | No keepalive yet, or no active peer session | Link if a session is live (handshake); broken chain if none/resolved |

`dead` is sticky: incoming messages from that peer are ignored. Only a new session start resets to `null`.

Expand Down
21 changes: 11 additions & 10 deletions ON_CHAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ unroll is extracted from the on-chain conditions.

### Step 2: Preempt or Wait

The player compares the on-chain unroll state number against their own latest
state to decide whether to preempt or wait for the timeout path (see
[Preemption](#preemption)). Both outcomes produce the same result: the unroll
coin is spent, creating game coins and reward coins.
`channel_coin_spent` classifies the parsed channel-coin spend (see
[Preemption](#preemption)). Timeout and preempt both spend the unroll coin,
creating game coins and reward coins. A spend we never signed (unknown puzzle
hash) or whose conditions do not match the signed historical record is an
error at parse time — it is not turned into a timeout.

When the unroll coin spend is detected, a `ChannelStatus` notification with
state `ResolvedUnrolled` (or `ResolvedStale` if the unroll was stale) is
Expand Down Expand Up @@ -347,12 +348,12 @@ a player sees the channel coin being spent to an unroll coin, they compare the
on-chain sequence number against their own latest state:


| On-chain SN vs ours | Action | Explanation |
| ------------------- | ----------------------- | ----------------------------------------------------------------------------------- |
| On-chain < ours, opposite parity | **Preempt** (immediate) | Spend the unroll coin immediately with our higher SN and more up-to-date conditions |
| On-chain < ours, same parity | **Wait for timeout** | The parity rule forbids our latest state from preempting this coin; use its compact historical timeout record |
| On-chain == ours | **Wait for timeout** | The unroll is at the state we expect; wait for it to resolve |
| On-chain > ours | **Error** | We've been hacked or something went very wrong |
| On-chain SN vs ours | Action | Explanation |
| ------------------- | ------ | ----------- |
| On-chain < ours, opposite parity, signed preemption source | **Preempt** (immediate) | Spend the unroll coin immediately with our higher SN and more up-to-date conditions |
| On-chain < ours, same parity (or no signed preemption source) | **Wait for timeout** | The parity rule forbids preemption; use the compact historical timeout record we signed |
| On-chain == ours | **Wait for timeout** | The unroll is at the state we expect; wait for it to resolve |
| Unknown puzzle hash or conditions-hash mismatch | **Error** | Never signed, or not the unroll we signed — classified when the channel coin spend is parsed |


Preemption is **immediate** — no timelock. This is by design: the preempting
Expand Down
14 changes: 9 additions & 5 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,15 @@ conditions. The map deliberately does not retain historical signatures or
preemption conditions; preemption always uses a latest full record. When a
channel coin spend is detected, the `CREATE_COIN` puzzle hashes in the
on-chain conditions are matched against this map to identify which unroll
landed. An old opposite-parity state is preempted, while an old same-parity
state is resolved with its stored timeout conditions. Those old puzzle hashes
cannot be discarded: the opponent may publish any previously signed unroll,
so recognizing every historical hash is the minimum needed to choose the
correct timeout record safely.
landed. Classification happens then, in `channel_coin_spent`: an old
opposite-parity state is preempted, an old same-parity state we signed is
resolved with its stored timeout conditions, and a spend we never signed
(unknown puzzle hash) or whose conditions do not match the signed record is
an error. A state we have not reached cannot be in the map, so it fails the
same never-signed check. Those old puzzle hashes cannot be
discarded: the opponent may publish any previously signed unroll, so
recognizing every historical hash is how we tell a signed timeout record
from a spend we never signed.

Browser session persistence stores the serialized game session as raw binary in
IndexedDB. Compact historical unroll records are therefore part of the durable
Expand Down
18 changes: 10 additions & 8 deletions UX_NOTIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ advisory, coin identity and amount, both balances, game allocation,
`havePotato`, `zeroPayout`, and optional on-chain progress context. During an
unroll, `unrollInitiator` identifies whether we or the opponent caused the
observed channel spend when that attribution is definitive; a locally queued
spend or cooperative-close setup alone leaves it unknown. `semanticPhase` refines the existing `GoingOnChain` /
`Unrolling` state as submitting or resolving the channel spend, preempting,
waiting for the relative timeout, submitting the timeout finish, or resolving
the unroll spend. These are display facts, not new lifecycle states. Banner text, the potato indicator, dashboard
spend or cooperative-close setup alone leaves it unknown. `semanticPhase` is
the situation within `GoingOnChain` / `Unrolling`: submitting or resolving a
channel spend, finding the landed unroll state, preempting, waiting for the
relative timeout, or spending the timeout finish. Actor is not encoded in the
phase. These are display facts, not new lifecycle states. Banner text, the potato indicator, dashboard
actions, phase selection, persistence, and restore all project from that one
snapshot instead of maintaining parallel channel-status shapes.
During a cooperative terminal handoff, Rust sets
Expand All @@ -226,12 +227,13 @@ Monotonicity applies across all three lenses:
When a watched timeout spend becomes mature, `TransactionManager` is the sole
component that queues its submission. Before the host drains the submission
buffer, it updates the session's canonical status snapshot to
`submitting_timeout_finish`; the normal `ChannelStatus` notification then
persists and restores that fact. The UI never infers timeout maturity, submits
`finishing_spending` (with `unrollInitiator` naming who started the unroll);
the normal `ChannelStatus` notification then persists and restores that fact. The UI never infers timeout maturity, submits
the transaction, or mutates a durable channel snapshot from a transient event.
If a reorg changes or clears the watched coin's birthday, the manager re-arms
the relative timeout and restores the canonical phase to `waiting_timeout`
until the claim becomes mature again.
the relative timeout and restores the canonical waiting phase
(`finishing_waiting_timeout`) until the claim becomes mature
again.

---

Expand Down
Loading
Loading