← all epics

Connection and sync — never pretend to be online

client agent-ready

repo: land-of-lor (game/world client + Area server, game/director) and this workspace (board, deliverables, site/play) · depends on: world-map-and-portalsground-types · updated 2026-08-19

What this is

Kris played v10 and v11 on the phone (APK) and in a browser, and the two views of the Village diverged: the other body stopped moving in one client and never appeared in the other, and the APK walked straight through portal rings — which are entirely server-driven. The server had the APK’s character barely moved. The transport was fine (a native headless client from the laptop, same TLS + Caddy path, gets every move acked, portal hop included). What was wrong was how the client behaves when its channel dies — and, in Kris’s words, the deeper thing: we do want the game to be able to be played offline, that is important, but we don’t ever want to fake that we are online when we are not — which is kind of what the current behaviour looks like.

This epic is the answer to both halves. It comes out of a full review of the client, the Area server, the director and the platform (three passes, 2026-08-18) whose findings are the task list; the plan that produced it is summarised in tasks/001 and each task’s own file.

The diagnosis, in short

game/world/client/client_main.gd: expecting_game_close is a sticky file-global, armed on every reconnect and portal hop and only ever consumed by a disconnect on a peer that has already been replaced and is never polled again — so the next real drop is swallowed with no rejoin, no reset, no message. The client keeps predicting into the void, walks through portals, sees nobody. Any portal arrival arms it; a phone’s radio hiccup or background pause springs it. The 016 liveness watchdog then made it near-deterministic: its receive timestamp is never seeded on connect, so on any real network it kills a healthy connection first, forcing the reconnect that arms the flag. Every automated test runs on localhost, where the first packet lands in the same frame and hides both.

Around that root cause the review found the same species everywhere: silent no-op sends and misleading logs; kick reasons (TAKEN_OVER, BAD_GRANT) sent and closed in one tick, which this Godot client cannot read (the very race 012 fixed for portal_result); intents overwritten per tick with no ack or reject; a peer left unbound answering NOT_HELLO forever with nothing prompting a re-hello; a portal transfer with no timeout; a snapshot pipeline that wedges silently; an Area process that exits without a word to its players; and — on the director — an Area that can be shut down under a live player because presence is counted on the director socket, not the game one (the 016 heartbeat made that more likely, not less). Fifteen constructible failure states, most with no self-heal, all indistinguishable in production logs from “the phone’s network died”.

Principles

Decisions (with Kris, 2026-08-18)

Non-goals

Relevant docs

Status

1 ready1 backlog6 done

Next: 006 — local play, the home when there is no realm — is ready (005 signed off, so its blocker is gone); 008 last.

Deliverables (1)

Tasks (8)

ready (1)

backlog (1)

done (6)

Ask an agent about this epic

    Also available as raw markdown.