---
id: connection-and-sync/003
title: Server truth — every intent answered, every kick delivered, no Area shut down under a player
epic: connection-and-sync
state: done
priority: 1
blocked_by: []
estimate: L
created: 2026-08-18
updated: 2026-08-18
claimed_by: null
claimed_at: null
delivers: []
review_artifact: land-of-lor/game/world/area_server.gd
---

## What to do

**Area server** (`game/world/area_server.gd`):
- Intents are processed **in order** within the movement budget, or the overwritten ones are
  rejected with `SUPERSEDED` — the client can reconcile every seq. (Today the per-tick
  Dictionary keeps only the last one and says nothing about the rest.)
- `_send_raw_to_peer` failure ⇒ log and drop the peer (its body stays).
- A peer left unbound (portal step 1, `_drop_other_entities_for_account`, takeover) is told —
  `error {REHELLO}` / `PORTAL_IN_PROGRESS` — instead of answering `NOT_HELLO` forever.
- Pre-hello peers receive nothing but `hello_ack` / errors (closes the unauthenticated feed).
- `pending_transfers` get a timeout: restore the entity at `from_cell` (010's restore path) and
  send `portal_blocked {TIMEOUT}`.
- The snapshot ack wedge (`pending_ack_seq`): retry on a timer; never block persistence
  forever on one seq.
- `area_closing {reason}` to every peer, then a grace, before **any** `quit()` (shutdown, retry
  exhaustion, control error) — and the control-error `quit(1)` guarded to the stale-spawnToken
  case only.
- `area_snapshot` (or a periodic `area_presence`) carries `connectedPeers`.

**Director** (`game/director/src/areaInstance.ts`, `server.ts`):
- `shutdownInstance` only when the world reports zero connected peers; the refcount becomes
  advisory (001's grace stays as belt-and-braces); portal arrival is covered by that rule.
- A `close` handler and a heartbeat on the control listener → `instance.registered = false`,
  `sendToArea` returns false and callers answer the client at once (`GRANT_FAILED` now, not
  after a 5 s stall).
- Every portal-transfer failure branch replies to the origin (`portal_transfer_rejected
  {reason}`), the exception catch included.
- Join-grant TTL 30 s → 90 s; the `completedTransfers` cache bounded and expiring.

## Definition of done

`area-server.test.ts`: three intents in one tick → three answers; `put_packet` failure drops
the peer and keeps the body; an unbound peer is told; `TAKEN_OVER` reaches a Godot client
before the close. Director tests: peer-count-based shutdown; portal-arrival presence;
`resume_portal` idempotence; control-close handling; every portal failure branch replies;
`area_closing` reaches peers. All existing suites and verifiers green.

## Where the work lands

`land-of-lor/game/world/area_server.gd`, `game/director/src/`, `game/director/test/`.

## What was done (2026-08-18, laptop session)

- `area_server.gd`: per-entity intent queue (one applied per tick, `TOO_FAST` past eight,
  `ENTITY_GONE`, `PORTAL_IN_PROGRESS` for moves queued behind a door or sent mid-transfer);
  `REHELLO` to a peer unbound by `_drop_other_entities_for_account`; broadcast only to
  bound peers; `_sweep_stale_transfers` (15 s → restore + `portal_blocked {TIMEOUT}`);
  snapshot resend after two intervals unacked; `area_closing` + 600 ms grace before any
  `quit()`, control-error quit guarded to `INVALID_REGISTRATION`; presence
  (`connectedPeers`, `boundAccounts`) on every hello / disconnect / 5 s and on snapshots.
- Director: `AreaPresence` schemas; `applyPresence`; `leave` keeps an Area up while the world
  reports someone else bound and shuts it down on the first empty report; drop-grace
  cancelled by presence; `controlSocketClosed`; `sendToArea` → bool, grant registration
  fails fast; every transfer failure replies `INTERNAL`; grants 90 s; the completed-transfer
  cache bounded (500) and expiring (1 h).
- Tests: three intents in one tick → three answers + `TOO_FAST`; presence keeps an Area up
  after the director socket drops and shuts it down when the game socket goes;
  `area_closing` observed before a graceful exit; all suites green.
- Not done: a test for the `REHELLO` unbind path (needs a duplicate-body arrival);
  `put_packet` failure → drop peer (kept at log-first — dropping on a transient full buffer
  seemed worse than a logged one; revisit in 007's soak).