diff --git a/multiplayer-next.md b/multiplayer-next.md index 792578e9..37d25f0d 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1204,7 +1204,7 @@ production fallback. |---|---|---| | 8.14 `[D:8.4,8.5,8.8]` | **IN PROGRESS.** Pure Go queue policy and PostgreSQL enforce one active ticket per verified player, 10 s heartbeat/30 s expiry, retry-safe owner/revision-scoped create/heartbeat/cancel, and deterministic candidate projection. Client cancellation is limited to `QUEUED`/`PROPOSED`; it cannot overwrite match-owned `ACCEPTED` through `LIVE` lifecycle states. A locked rejection classifier maps missing ticket, wrong owner, expiry, stale revision, and invalid state to distinct domain/API outcomes without weakening the atomic mutation predicate. Queue admission also honors both pre-live and live ranked abandonment penalties, so an expired reconnect cannot immediately requeue after result completion. Redis is an optional rebuildable projection over authoritative PostgreSQL | Domain/store/API tests cover ownership, expiry, idempotency, candidate binding, exact mutation-state fences, live-ticket cancellation rejection, stale revision classification, abandonment cooldown selection, concurrent create/heartbeat races, durable-source cache repair, Redis TTL/lost-keyspace behavior, and playlist/build/protocol compatibility. PostgreSQL-tagged lifecycle regressions compile and prior live runs cover the queue races; live database reruns remain blocked by Docker storage. Live Redis failover-under-load and worker integration remain | | 8.15 `[D:7.8,8.3]` | **IN PROGRESS.** Pure Go probe validation treats Steam location as opaque, requires nonce/freshness/region and server-computed RTT, and implements discrepancy quarantine/release; authenticated HTTP now accepts only opaque location/nonce input through a server-owned probe provider and records validated RTT into the active player's durable queue ticket; durable queue projections have a server-derived RTT JSON field for matcher reads | `server/domain/probes.go`, `server/migrations/0003_queue_probe_metadata.sql`, adversarial fixtures and `server/api/service.go`/`store/queue_sql.go` cover stale/wrong/forged evidence, the 25 ms/30% threshold, three-sample quarantine, five-clean release, authenticated provider arguments, rejection of client RTT fields, player/ticket/expiry binding, persistence failure, playlist-scoped candidate reads and bounded metadata decoding; Steam coordinator, regional probe adapters and multi-region probe population remain | -| 8.16 `[D:8.14,8.15]` | **IN PROGRESS.** Pure Go candidate/team selection implements the <=100 ms ceiling, pairwise widening tolerance, anchor inclusion, deterministic set/region scoring and balanced team partitioning; queue-backed formation now consumes the server-owned projection, fences duplicate player identities and rejects playlist/build/protocol mixing; the matcher worker performs bounded formation and delegates the final claim to the durable proposal transaction; the runnable matcher now supports explicitly enabled ranked six-player polling with durable verified-Steam identity lookup. **Fixed a real crash-loop**: `Worker.Run` treated every `RunOnce` error as fatal to the whole loop, including "no compatible candidates" (`FormFromQueue`'s completely routine answer when currently-queued players share no verified region) — found building a live two-player integration attempt (see below): two real players with no common region crashed the entire matcher process, taking matchmaking down for every other player in the playlist, and would crash-loop again on restart since the same incompatible candidates stay queued. Now only genuine static misconfiguration (`ErrWorkerNotConfigured`/`ErrUnsupportedPlaylist`/`ErrInvalidMatcherSize`) stops the loop; everything else retries next interval. **Fixed a second, quieter wedge in the same area**: `FormFromQueue`'s anchor is always the single oldest candidate, deterministically, so when `domain.PrepareProposal` rejected that exact formation for a reason specific to those particular players (mismatched protocol, incomplete ranked identity metadata, a duplicate-SteamID pair) rather than "no compatible batch exists", `RunOnce` returned immediately and the next interval reproduced the identical formation and failed again — forever, permanently head-of-line-blocking every other waiting player behind that anchor too, not just the players actually at fault (this is the "innocent-ticket restoration" gap task 8.20 named: the innocents were never stuck in the database, since no claim had happened yet, but they were durably starved of ever being tried). `RunOnce` now excludes a failed formation's players and retries with the remaining pool, bounded to 8 attempts per pass; a batch that has no viable formation at all (the pre-existing no-common-region case) still returns immediately rather than looping pointlessly. **This fix was inert without a companion one**: `RunOnce` was asking `Source` for exactly `w.Size` candidates -- `SelectCandidates` was always designed to search a larger pool (it takes an anchor plus an arbitrary remainder and widens through it), but the call site never gave it one, so there was never a "remainder" for the exclusion retry to fall back to in production. `RunOnce` now requests up to 10x `w.Size` (capped at 200) instead | `server/domain/matcher.go`, `teams.go`, `server/matcher/worker.go`, `server/store/queue_sql.go` and adversarial fixtures cover no-common-region, tolerance boundaries, lexical ties, mean-rating balance, malformed candidates, duplicate identities, compatibility mismatches, incomplete batches, source failure, durable claim failure, queue-backed oldest-anchor formation and incomplete ranked identity metadata; two new tests cover `Run` (not just `RunOnce`) surviving a per-pass error via a real concurrent goroutine, and still stopping immediately on a real configuration error, both clean across repeated `-race` runs; three further tests cover the formation-exclusion retry (an 8-candidate batch whose oldest 4 are permanently doomed still forms and claims the remaining 4, excluding the doomed players from the claimed ticket set), that exhausting every attempt still surfaces the last real error rather than a silent `false,nil`, and that `Source` is actually asked for more than `w.Size` candidates. **The two-player Godot proposal integration now passes**: `Game/tests/control_plane_proposal_smoke.gd`/`.tscn` and `scripts/verify_control_plane_proposal_integration.sh` found the original crash-loop bug above; headless Godot testing was then paused for several sessions after a run of native engine crashes (macOS crash reporter, `EXC_BAD_ACCESS`/`SIGBUS`) that day. Reading the actual `~/Library/Logs/DiagnosticReports/Godot-*.ips` crash reports (rather than relying on temporal correlation) found every one of the 25 reports on the machine named `ChatGPT`/`codex` (17), an already-exited process under that same tree (6), or a manual `iTerm2` session (1) as the responsible/parent process — none named Claude Code. Godot testing was resumed on that evidence (with the user's explicit go-ahead) and re-verified clean: `test_runner.tscn` (212/212), the full `make verify-enet-integration` suite (all five cases including the 3-process match), `verify_control_plane_proposal_integration.sh` (passed twice, real matcher forms the proposal and both clients accept), and the complete `make verify-multiplayer-local` gate -- zero new crash reports across all of it. Arena selection and long-running worker integration remain | +| 8.16 `[D:8.14,8.15]` | **IN PROGRESS.** Pure Go candidate/team selection implements the <=100 ms ceiling, pairwise widening tolerance, anchor inclusion, deterministic set/region scoring and balanced team partitioning; queue-backed formation now consumes the server-owned projection, fences duplicate player identities and rejects playlist/build/protocol mixing; the matcher worker performs bounded formation and delegates the final claim to the durable proposal transaction; the runnable matcher now supports explicitly enabled ranked six-player polling with durable verified-Steam identity lookup. **Fixed a real crash-loop**: `Worker.Run` treated every `RunOnce` error as fatal to the whole loop, including "no compatible candidates" (`FormFromQueue`'s completely routine answer when currently-queued players share no verified region) — found building a live two-player integration attempt (see below): two real players with no common region crashed the entire matcher process, taking matchmaking down for every other player in the playlist, and would crash-loop again on restart since the same incompatible candidates stay queued. Now only genuine static misconfiguration (`ErrWorkerNotConfigured`/`ErrUnsupportedPlaylist`/`ErrInvalidMatcherSize`) stops the loop; everything else retries next interval. **Fixed a second, quieter wedge in the same area**: `FormFromQueue`'s anchor is always the single oldest candidate, deterministically, so when `domain.PrepareProposal` rejected that exact formation for a reason specific to those particular players (mismatched protocol, incomplete ranked identity metadata, a duplicate-SteamID pair) rather than "no compatible batch exists", `RunOnce` returned immediately and the next interval reproduced the identical formation and failed again — forever, permanently head-of-line-blocking every other waiting player behind that anchor too, not just the players actually at fault (this is the "innocent-ticket restoration" gap task 8.20 named: the innocents were never stuck in the database, since no claim had happened yet, but they were durably starved of ever being tried). `RunOnce` now excludes a failed formation's players and retries with the remaining pool, bounded to 8 attempts per pass; a batch that has no viable formation at all (the pre-existing no-common-region case) still returns immediately rather than looping pointlessly. **This fix was inert without a companion one**: `RunOnce` was asking `Source` for exactly `w.Size` candidates -- `SelectCandidates` was always designed to search a larger pool (it takes an anchor plus an arbitrary remainder and widens through it), but the call site never gave it one, so there was never a "remainder" for the exclusion retry to fall back to in production. `RunOnce` now requests up to 10x `w.Size` (capped at 200) instead | `server/domain/matcher.go`, `teams.go`, `server/matcher/worker.go`, `server/store/queue_sql.go` and adversarial fixtures cover no-common-region, tolerance boundaries, lexical ties, mean-rating balance, malformed candidates, duplicate identities, compatibility mismatches, incomplete batches, source failure, durable claim failure, queue-backed oldest-anchor formation and incomplete ranked identity metadata; two new tests cover `Run` (not just `RunOnce`) surviving a per-pass error via a real concurrent goroutine, and still stopping immediately on a real configuration error, both clean across repeated `-race` runs; three further tests cover the formation-exclusion retry (an 8-candidate batch whose oldest 4 are permanently doomed still forms and claims the remaining 4, excluding the doomed players from the claimed ticket set), that exhausting every attempt still surfaces the last real error rather than a silent `false,nil`, and that `Source` is actually asked for more than `w.Size` candidates. **The two-player Godot proposal integration now passes**: `Game/tests/control_plane_proposal_smoke.gd`/`.tscn` and `scripts/verify_control_plane_proposal_integration.sh` found the original crash-loop bug above; headless Godot testing was then paused for several sessions after a run of native engine crashes (macOS crash reporter, `EXC_BAD_ACCESS`/`SIGBUS`) that day. Reading the actual `~/Library/Logs/DiagnosticReports/Godot-*.ips` crash reports (rather than relying on temporal correlation) found every one of the 25 reports on the machine named `ChatGPT`/`codex` (17), an already-exited process under that same tree (6), or a manual `iTerm2` session (1) as the responsible/parent process — none named Claude Code. Godot testing was resumed on that evidence (with the user's explicit go-ahead) and re-verified clean: `test_runner.tscn` (212/212), the full `make verify-enet-integration` suite (all five cases including the 3-process match), `verify_control_plane_proposal_integration.sh` (passed twice, real matcher forms the proposal and both clients accept), and the complete `make verify-multiplayer-local` gate -- zero new crash reports across all of it. **"Arena selection" was also stale**: `domain.RankedArenaForProposal` selects the arena deterministically at proposal time for ranked, `agones.Client.Allocate` already requests it (and playlist/region/build/protocol/transport) as Agones annotations, and `supervisor.withAllocatedCompatibility` already overlays every one of those onto the allocated Godot process's launch flags -- overriding the Fleet's static defaults, since a shared pod template cannot vary per-match on its own -- fully tested (`supervisor_test.go`'s `TestWithAllocatedCompatibilityOverridesStaleFlagsAndRejectsUnsafeValues` proves stale static flags are overridden by live annotation values) and wired into `Supervisor.Start()`. Casual deliberately never sets an arena path at all (`proposal.ArenaPath` stays empty for `domain.Casual` in `formation.go`); the supervisor's flag-override is then a no-op and the allocated server falls back to its own `ArenaRegistry.path_for_match` rotation, the same mechanism the community server already used -- this was always the intended design for casual, not a gap. §8.41's "dynamic per-match launch flags... remain" note describing this same mechanism was equally stale and is corrected there too. Long-running worker integration remains | | 8.17 `[D:8.14,8.16]` | **IN PROGRESS.** Pure Go proposal policy sends a 10-second response window to every selected human, requires unanimous acceptance, applies exact casual/ranked decline and timeout cooldowns with ranked escalation, and exposes revisioned idempotent responses through the authenticated API. Proposal closure now atomically separates offenders from innocents: a decliner's ticket is `CANCELLED`; a timed-out player's ticket is `EXPIRED`; accepted or otherwise innocent participants return to `QUEUED` with their original `enqueued_at` and refreshed expiry. Direct queue cancellation closes the open proposal and requeues remaining participants immediately. Late API responses commit expiry, timeout penalties, and ticket release before returning `ErrProposalClosed`; recovery of an old declined proposal cannot misclassify its pending innocents as timeouts. Cooldown history rejects future, foreign-playlist, and invalid-kind events, and database rows are closed before penalty writes | Domain/store/API fixtures cover partial/unanimous response, expiry, replay/conflict, stale revision, exact cooldown windows/escalation, corrupt history filtering, offender ticket termination, innocent precedence preservation, direct-cancel cascade, and the former late-response rollback. PostgreSQL-tagged regressions compile and assert the durable split and penalty rows; the full local Go suite passes. Live PostgreSQL execution and allocation integration remain | | 8.18 `[D:8.5,8.14,8.17]` | **IN PROGRESS.** Go store layer defines PostgreSQL SERIALIZABLE whole-transaction retries and queue candidate/proposal claim SQL using `FOR UPDATE SKIP LOCKED` plus durable uniqueness/revision fences; proposal creation persists matcher-selected region/protocol/team/slot topology before acceptance, and the same serializable final-acceptance transaction now promotes the exact roster into one `ALLOCATING` match, closing the process-crash gap that could otherwise strand an accepted proposal before the former second promotion transaction. The API promoter remains a replay check. Promotion replay validates immutable playlist/region/protocol/arena, participant, ticket, team, and slot identity but deliberately ignores mutable match state/server ownership, so a retry after a lost response still succeeds after allocation has advanced. Result sets are closed before crossing into promotion writes, avoiding one-connection pool stalls. Redis remains a rebuildable candidate projection over PostgreSQL authority | Store/API tests cover retries, claims, owner/revision fencing, expiry, exact promotion replay/conflict, progressed-match replay, rollback of partial claims, concurrent contested-ticket formation, and lost-cache repair. PostgreSQL-tagged regressions compile and assert acceptance, ticket transitions, match creation, and roster insertion are one durable outcome; prior live runs covered queue/proposal promotion and races, while this atomic-promotion change awaits a live database rerun. Allocation runtime integration remains | | 8.19 `[D:8.18]` | **IN PROGRESS.** Pure Go casual lineup requires 2–6 humans with at least one per team, fills missing slots with explicit bots, permits kickoff-only bot-slot backfill and assigns no backfill penalty/rating update; proposal preparation now derives the lineup from formed teams | `server/domain/casual.go`, `formation.go` cover both-team minimum, bot shape, live-play rejection, zero-penalty backfill and casual proposal composition; queue candidate selection, opt-in 10 s backfill proposals, reconnect/leave penalties and live integration remain | @@ -1241,7 +1241,7 @@ The allocated-runtime result reporter now keeps a completed match in `RESULTS` u |---|---|---| | 8.39 `[D:8.3,8.14,8.17]` | **IN PROGRESS.** `MatchmakingState` now projects queue → proposal → allocation/process-ready/assignment-ready/connect/live plus terminal failure states; autoload `ControlPlaneClient` provides authenticated queue create/recovery/heartbeat/cancel and proposal response requests with idempotency/revision headers; `matchmaking.tscn`/`matchmaking.gd` expose the state and authoritative actions from the main menu; authenticated proposal recovery now reconciles missed proposal events and expires them at read time; the Go API publishes targeted authenticated revisioned queue/proposal/assignment events and the Godot client consumes state/proposal/assignment events, fetching the authoritative assignment after assignment readiness; the API can now use the durable participant-scoped proposal provider for both recovery and accept/decline mutations; durable allocation/no-show transitions write targeted state outbox rows and production/testkit dispatchers deliver them after commit; the client now explains queue wait progress and connection latency quality | `test_matchmaking_state.gd`, `test_control_plane_client.gd`, `test_matchmaking_ui.gd`, `TestProposalRecoveryIsParticipantScopedAndExpiresAtReadBoundary`, `TestAuthenticatedWebSocketDeliversOnlyTargetedRevisionedEvents`, `TestStateChangingAPIActionsPublishTargetedEvents`, `TestProposalRecoveryUsesDurableBackendAndRemainsParticipantScoped`, and state outbox tests reject stale/gapped/conflicting updates, validate endpoint/token/payload normalization, preserve idempotent duplicates, guarantee visible phase/terminal copy, and target every participant; live PostgreSQL-backed dispatcher/fan-out verification remains | | 8.40 `[D:8.3,8.14]` | **IN PROGRESS.** Pure Go revisioned replica reducer rejects gaps for REST resync, makes duplicate/out-of-order events idempotent, and resumes from the authoritative snapshot revision; authenticated queue-ticket recovery now has an owner-checked REST read; Godot client projection persists non-secret ticket/proposal state, forces authoritative recovery after restart, and can replay a lost queue-create response with the original ticket/idempotency key; the Go API now exposes an authenticated `/v1/events` WebSocket with bounded per-player queues, strict upgrade/vocabulary validation and REST-resync-safe slow-client failure; Godot `ControlPlaneClient` can connect to the stream using Godot 4.7's handshake-header API, consume validated events, automatically reconnect with bounded backoff, and trigger REST recovery on projection gaps and stream return, including a deferred recovery when an event arrives during an in-flight HTTP mutation; `OutboxDispatcher` now provides ordered at-least-once delivery after durable commit; assignment recovery events carry the durable assignment revision without leaking it into REST JSON; prediction startup now distinguishes the sequence-0 warm-up acknowledgement from genuine missing/evicted history, preventing a false hard-snap during a live match; proposal creation writes one durable targeted `proposal_changed` outbox event per proposal aggregate revision, while result completion writes `match_completed` and production `cmd/control-plane` plus the test-only API harness dispatch both event types through separate filtered consumers | `server/domain/sync.go`, `server/api/events.go`, `server/api/outbox.go`, `server/api/service.go`, `server/store/outbox.go`, `server/store/proposal_sql.go`, `server/cmd/control-plane`, `server/cmd/testkit-api`, `service_test.go`, `outbox_test.go`, `matchmaking_state.gd`, `control_plane_client.gd`, `local_prediction_history.gd`, `net_ship_predictor.gd` and tests cover gap, snapshot, replay, same-revision conflict, owner-only ticket recovery, expired-ticket terminal handling, malformed restart snapshots, API-level duplicate-create replay/conflict, authenticated handshake/key rejection, targeted event delivery, exactly-once slow-subscriber closure, invalid-event rejection, delivery-before-ack failure ordering, assignment event/response revision separation, prediction warm-up/hard-resync separation, deferred proposal recovery and proposal/result outbox filtering and delivery failures; Godot 4.7.1 headless project parse and 150-test unit harness pass with compatibility rendering, and a two-process ENet authoritative match smoke passes spawn, movement, prediction, 0% snapshot loss and 0 hard snaps; `cmd/control-plane` provides a signal-bound API role, `cmd/matcher` now supports casual and explicitly enabled ranked roles with durable identity lookup, and `cmd/maintenance` provides bounded season maintenance. **Live multi-process control-plane/game verification now exists**: `scripts/verify_control_plane_integration.sh` runs a real `postgres:17-alpine`, the real `api.Service` (via the new test-only `server/cmd/testkit-api`, wired identically to `cmd/control-plane` except for a fake Steam login — see §8.7), and a real headless Godot client (`control_plane_smoke.gd`) round-tripping login → `fetch_ranked_profile` (expect 404, §8.22) → `queue_create` → heartbeat → `cancel_queue` over an actual network connection — the first time this boundary was exercised end to end rather than against a mock on either side. **The two-player proposal extension now also passes**: `scripts/verify_control_plane_proposal_integration.sh` starts isolated PostgreSQL, the real matcher and testkit API, launches two headless Godot clients, seeds deterministic RTT projections, observes the real WebSocket `OPEN` proposal, accepts from both clients, and verifies both durable tickets plus the proposal reach `ACCEPTED` (including authoritative recovery when the concurrent revision advances). `scripts/run_result_fanout_integration.sh` additionally verifies a real PostgreSQL-backed authenticated WebSocket receives a completed-match event; allocator and Redis fan-out live verification remain | -| 8.41 `[D:7.8,8.9,8.31,8.40]` | **IN PROGRESS.** Authenticated `GET /v1/assignments/{matchId}` now exposes only a validated, player-scoped assignment view including the hosted endpoint; Godot `AssignmentState`/`ControlPlaneClient.fetch_assignment()` bind the response to the authenticated player, recheck expiry, validate and retain the endpoint, and `connect_to_assignment()` starts only the validated ENet/Steam transport after assignment readiness. **Found and fixed a severe gap this row had previously described as already closed**: `connect_to_assignment()` existed, fully validated, with its own `assignment_connection_started`/`assignment_connection_failed` signals -- but nothing anywhere in the client ever called it. A player who completed the entire queue -> proposal -> allocate -> assign pipeline would reach `ASSIGNED` and see "Your match server is ready" and then simply sit there forever; the transport was never actually started. `ControlPlaneClient._connect_when_assigned()` now calls it automatically the moment `state.phase` reaches `ASSIGNED` (wired into the one call site every queue-shaped HTTP response -- heartbeat, recover, and resync-triggered recover -- already shares, so both the REST poll and the WebSocket-triggered-resync path are covered without a second call site), deferring via `_pending_connect_match_id` if the assignment fetch triggered earlier by `ASSIGNMENT_READY` hasn't completed yet, and guarding against a duplicate/replayed `ASSIGNED` event reattempting the connection. The opaque join authorisation is carried in the MatchNet hello payload rather than the endpoint URL; allocated Godot servers now fail closed unless an operator-mounted JSON roster of control-plane signed envelopes and an HMAC-SHA256 key are present, and MatchNet verifies the canonical Go claim bytes, checks exact token membership plus match/server/protocol/slot/expiry claims before admitting a peer, rejects concurrent reuse of an active token, tracks server-owned reconnect generations across the 60-second reclaim window, fences expired reclaims and invalidates old peers; migration 0002 and the Go store adapter now persist/recover the complete player-scoped assignment projection with conflict-safe identical replay; `AssignmentProviderFromStore` wires that durable projection into the API injection point; `SaveAssignments` publishes a complete signed roster atomically instead of allowing partial player visibility; `SaveVerifiedAssignmentRoster` rechecks signed claims before deriving player rows; a workload-authenticated `GET /v1/servers/{serverId}/roster` now returns the complete signed envelope set only for the bound allocation, and the allocated supervisor atomically materializes it before launching Godot; live authenticated Godot/PostgreSQL assignment verification now exists via `scripts/verify_assignment_integration.sh`, which seeds a durable player-scoped `ASSIGNMENT_READY` row behind the real testkit API and verifies the returned endpoint/join authorisation over HTTP/JSON (including the live numeric-decoding boundary); `scripts/run_supervisor_integration.sh` additionally verifies the allocated-server process-ready → roster fetch/materialization → assignment-ready registration path against real PostgreSQL and the real API, with the workload token recovered from an Agones-shaped GameServer annotation | `server/api/service.go`, `store_adapters.go`, `service_test.go`, `assignment_state.gd`, `control_plane_client.gd`, `match_net.gd`, `server_boot.gd`, `server_config.gd`, `test_assignment_state.gd`, `test_control_plane_client.gd`, `test_match_net.gd`, `server/contracts/v1/openapi.json`, `server/migrations/0002_assignments.sql`, `server/allocator/allocator_integration_test.go`, `server/supervisor/supervisor_integration_test.go`, `server/supervisor/supervisor.go` and `server/store/assignment_sql.go` cover participant/identity/expiry/shape/transport/endpoint boundaries, player-scoped schema keys, expiry-filtered reads, assignment upsert conflict handling, atomic batch validation, signed-claim binding, strict endpoint splitting, allowlisted claim rejection, canonical HMAC interoperability, forged-signature rejection, duplicate active-token rejection, reclaim generation/expiry behavior, workload-authenticated roster delivery, atomic file installation, real allocator reconciliation, real supervisor registration and 150-test Godot compatibility coverage; the three-process disconnect/reclaim smoke passes ship retention, replacement drive and old-peer invalidation; two new `test_control_plane_client.gd` tests cover the connect-wiring fix directly: `test_client_starts_the_transport_once_the_ticket_reaches_assigned` proves a ready, fresh assignment plus an `ASSIGNED` ticket update actually starts the transport (`state.phase` advances to `CONNECTING`, `connect_to_assignment`'s own signal fires) and that a duplicate attempt is refused, `test_client_defers_the_connect_until_the_assignment_fetch_completes` proves the opposite ordering (an `ASSIGNED` update before the assignment fetch completes) defers rather than either connecting with stale data or erroring; verified against the real Godot 4.7.1 binary (216/216, no crash), the full local gate and the ENet integration suite; dynamic per-match launch flags, SDR relay-ticket installation and live Agones cluster integration remain | +| 8.41 `[D:7.8,8.9,8.31,8.40]` | **IN PROGRESS.** Authenticated `GET /v1/assignments/{matchId}` now exposes only a validated, player-scoped assignment view including the hosted endpoint; Godot `AssignmentState`/`ControlPlaneClient.fetch_assignment()` bind the response to the authenticated player, recheck expiry, validate and retain the endpoint, and `connect_to_assignment()` starts only the validated ENet/Steam transport after assignment readiness. **Found and fixed a severe gap this row had previously described as already closed**: `connect_to_assignment()` existed, fully validated, with its own `assignment_connection_started`/`assignment_connection_failed` signals -- but nothing anywhere in the client ever called it. A player who completed the entire queue -> proposal -> allocate -> assign pipeline would reach `ASSIGNED` and see "Your match server is ready" and then simply sit there forever; the transport was never actually started. `ControlPlaneClient._connect_when_assigned()` now calls it automatically the moment `state.phase` reaches `ASSIGNED` (wired into the one call site every queue-shaped HTTP response -- heartbeat, recover, and resync-triggered recover -- already shares, so both the REST poll and the WebSocket-triggered-resync path are covered without a second call site), deferring via `_pending_connect_match_id` if the assignment fetch triggered earlier by `ASSIGNMENT_READY` hasn't completed yet, and guarding against a duplicate/replayed `ASSIGNED` event reattempting the connection. The opaque join authorisation is carried in the MatchNet hello payload rather than the endpoint URL; allocated Godot servers now fail closed unless an operator-mounted JSON roster of control-plane signed envelopes and an HMAC-SHA256 key are present, and MatchNet verifies the canonical Go claim bytes, checks exact token membership plus match/server/protocol/slot/expiry claims before admitting a peer, rejects concurrent reuse of an active token, tracks server-owned reconnect generations across the 60-second reclaim window, fences expired reclaims and invalidates old peers; migration 0002 and the Go store adapter now persist/recover the complete player-scoped assignment projection with conflict-safe identical replay; `AssignmentProviderFromStore` wires that durable projection into the API injection point; `SaveAssignments` publishes a complete signed roster atomically instead of allowing partial player visibility; `SaveVerifiedAssignmentRoster` rechecks signed claims before deriving player rows; a workload-authenticated `GET /v1/servers/{serverId}/roster` now returns the complete signed envelope set only for the bound allocation, and the allocated supervisor atomically materializes it before launching Godot; live authenticated Godot/PostgreSQL assignment verification now exists via `scripts/verify_assignment_integration.sh`, which seeds a durable player-scoped `ASSIGNMENT_READY` row behind the real testkit API and verifies the returned endpoint/join authorisation over HTTP/JSON (including the live numeric-decoding boundary); `scripts/run_supervisor_integration.sh` additionally verifies the allocated-server process-ready → roster fetch/materialization → assignment-ready registration path against real PostgreSQL and the real API, with the workload token recovered from an Agones-shaped GameServer annotation | `server/api/service.go`, `store_adapters.go`, `service_test.go`, `assignment_state.gd`, `control_plane_client.gd`, `match_net.gd`, `server_boot.gd`, `server_config.gd`, `test_assignment_state.gd`, `test_control_plane_client.gd`, `test_match_net.gd`, `server/contracts/v1/openapi.json`, `server/migrations/0002_assignments.sql`, `server/allocator/allocator_integration_test.go`, `server/supervisor/supervisor_integration_test.go`, `server/supervisor/supervisor.go` and `server/store/assignment_sql.go` cover participant/identity/expiry/shape/transport/endpoint boundaries, player-scoped schema keys, expiry-filtered reads, assignment upsert conflict handling, atomic batch validation, signed-claim binding, strict endpoint splitting, allowlisted claim rejection, canonical HMAC interoperability, forged-signature rejection, duplicate active-token rejection, reclaim generation/expiry behavior, workload-authenticated roster delivery, atomic file installation, real allocator reconciliation, real supervisor registration and 150-test Godot compatibility coverage; the three-process disconnect/reclaim smoke passes ship retention, replacement drive and old-peer invalidation; two new `test_control_plane_client.gd` tests cover the connect-wiring fix directly: `test_client_starts_the_transport_once_the_ticket_reaches_assigned` proves a ready, fresh assignment plus an `ASSIGNED` ticket update actually starts the transport (`state.phase` advances to `CONNECTING`, `connect_to_assignment`'s own signal fires) and that a duplicate attempt is refused, `test_client_defers_the_connect_until_the_assignment_fetch_completes` proves the opposite ordering (an `ASSIGNED` update before the assignment fetch completes) defers rather than either connecting with stale data or erroring; verified against the real Godot 4.7.1 binary (216/216, no crash), the full local gate and the ENet integration suite. **"Dynamic per-match launch flags" was stale, corrected in §8.16**: `agones.Client.Allocate` already requests arena-path/playlist/region/build/protocol/transport as Agones annotations and `supervisor.withAllocatedCompatibility` already overlays them onto the launch command, fully tested and wired into `Supervisor.Start()`. SDR relay-ticket installation and live Agones cluster integration remain | | 8.42 `[D:8.22,8.23,8.24,8.40]` | **IN PROGRESS.** `RankedProfileState` and `ControlPlaneClient.fetch_ranked_profile()` expose the backend-authoritative rating/RD/volatility/games/tier/provisional/season view; matchmaking UI displays provisional/tier status without client-side rating math; runnable API binaries now wire the backend-owned default tier policy; live authenticated profile verification now exists via `scripts/verify_ranked_profile_integration.sh`, which seeds a durable rating and verifies the populated profile over real PostgreSQL, Go HTTP, and Godot JSON | `test_control_plane_client.gd` validates profile shape, numeric safety and provisional display; the real populated profile path now covers durable rating/games/tier/provisional decoding and caught the missing runtime tier-policy wiring; committed revision after reconnect, abandon status and season countdown remain dependent on live auth/backend events and Godot runtime verification | | 8.43 `[D:8.39,8.40,8.41]` | **IN PROGRESS.** Matchmaking client now distinguishes expired queue recovery, session expiry, missing records and retryable control-plane outages; a 401 clears the in-memory token, emits `session_expired` and disables retry until a new session is configured; terminal messages remain visible and active searches are not falsely failed on transient errors; stale proposal accept/decline conflicts now automatically schedule an authoritative proposal recovery instead of requiring each UI caller to implement 409 handling | `MatchmakingState` and `ControlPlaneClient` tests cover explicit expiry and the existing terminal/retry-safe state paths; the real two-player proposal smoke now exercises the client-owned conflict-recovery path. **This row's own "remain" list was stale**: `matchmaking.gd`'s decline button/handler already existed (`%DeclineButton`, `_on_decline_pressed`, visibility toggled by `MatchmakingState.PROPOSED` alongside accept) with no test gap; `ControlPlaneClient.can_retry_last_mutation()`/`retry_last_mutation()` -- the generic "duplicate-action recovery beyond proposals" and "regional outage retry" mechanism -- also already existed (any mutation, not just a proposal response, becomes retryable on a transport failure or 503/408/429, and the matchmaking queue button already fell back to it), it simply had zero test coverage proving the transition actually happens for a non-proposal mutation; two new tests close that (`test_generic_mutation_retry_recovers_after_a_transient_failure`, `test_generic_mutation_retry_is_not_offered_for_unsafe_failures`). `MatchmakingClient`'s real dispatch (`HTTPRequest.request()`) needs a live SceneTree that `test_runner.tscn`'s synchronous single-`_ready()` execution model cannot provide mid-suite, so the two new tests exercise the `can_retry_last_mutation()` decision boundary and the `ERR_INVALID_DATA` fail-closed path rather than the literal network call; verified against the real Godot 4.7.1 binary (214/214 tests, no crash, no engine-level error), plus a full `make verify-multiplayer-local` re-run. Version-mismatch-specific messaging (a protocol rejection currently surfaces only as the server's generic error string, not a distinguished "update your client" affordance), failed-reconnect UX, and arena selection/long-running worker integration (§8.16) remain |