feat(client+engine): D-254 Atlas companion wave 1 — reader connections + standalone shell + make atlas #183

Closed
jpmschweitzer wants to merge 0 commits from atlas-companion-app into main
Owner

Summary

Wave 1 of the Atlas companion app (D-254, epic T-1128): the decision record, the server's reader-connection layer, and the standalone desktop shell with make atlas. After this merges, make atlas gives a read-only second-monitor Atlas against a running server (attach) or a self-spawned one — the legitimate player-facing pattern Jeroen specified, and the lightweight DB-scanning tool for development.

T-1129 — D-254 decision record (design pass)

Full architecture record in governance/decisions/architecture.md: connection model (attach SR_PORT/9876 else spawn --port 0), ConnectionRole reader protocol with permitted-message matrix, wire-only data browser (no SQLite GDExtension, D-020; T-949 precedent), TradingReader future seam, loopback-only scope, save/load interaction seam (Phase 5+), and the SS3 amendment: the companion is a generic implant HOST — every registered implant app appears automatically; available_in_companion: bool = true on the app manifest is the opt-out for gameplay-dependent apps; the in-game implant ignores the flag entirely.

T-1130 — reader connections (server, Oscar)

  • ConnectionRole (Player | Reader, #[serde(default)] = Player — old clients wire-compatible byte-for-byte; shaped for a future TradingReader).
  • BridgeResource rewritten 0-1 Player + 0-N Readers with ConnectionId; per-tick accept_new_connections loop replaces the single blocking accept (the old accept made any second connection hang the client and its disconnect kill the server).
  • Permitted-message matrix enforced: readers handshake + issue Atlas/StarMap/CityNames requests (responses connection-tagged, own requests only); PlayerInput from a reader = drop + strike (disconnect at 3); ObserverSnapshot has no reader-facing send path at all (structural, not filtered).
  • Shutdown scope: only a Player send failure flips ServerRunning. Reader-only servers (spawn-mode's common case — fixed from the original design during implementation) idle validly; reader disconnects never kill a session.
  • 6 new bridge_tcp integration tests + layer3 subprocess end-to-end; 79 tests exercised, full lib suite green.

T-1132 — standalone shell + make atlas (client, Stig)

  • atlas_standalone scene/script: attach-or-spawn boot. One real connect_to_sim attempt — the originally-designed throwaway TCP probe was proven by live run to kill a pre-accept-loop server (probe consumed the blocking accept slot; server's handshake hit broken pipe and exited). Never abandon a connected socket.
  • Spawn path: ServerProcess.start_with_pipe (new method, existing API untouched) + LISTENING:{port} stdout parse, real systems.db, no --test-mode.
  • Reader role end to end: encode_startup_message optional role param (empty omits the key — byte-identical output for every existing caller, matching serde default); client suppresses RequestAllSettings and AutoPause/AutoResume auto-sends for readers (each would burn a violation strike — endorsed by Oscar cross-review).
  • Generic implant host (D-254 SS3): shell instantiates ALL registered implant apps, filtered by available_in_companion; registry default parameter preserves in-game behavior byte-identically. Future implant apps extend the companion automatically.
  • Owned-server lifecycle: spawning companion kills its child on close; attached servers survive companion close (verified live). Known engine limitation documented in-code: raw SIGTERM bypasses Godot notifications entirely and orphans a spawned server — WM close paths verified clean repeatedly. Follow-up T-1136 (server-side parent-death watchdog) filed.

Live verification (real builds, real display)

Spawn-mode boots and renders the live star map (301 systems over the wire); body browse renders full orbital layouts; attach-mode connects first-try against an independent server; two simultaneous readers verified with independent lifecycles; zero orphan processes after every graceful close including make atlas itself.

Out of scope (wave 2, already ticketed)

T-1131 browse data proxy, T-1133 data browser UI, T-1134 seed picker, T-1135 character-spawn split.

Tickets: T-1129, T-1130, T-1132

## Summary Wave 1 of the Atlas companion app (D-254, epic T-1128): the decision record, the server's reader-connection layer, and the standalone desktop shell with `make atlas`. After this merges, `make atlas` gives a read-only second-monitor Atlas against a running server (attach) or a self-spawned one — the legitimate player-facing pattern Jeroen specified, and the lightweight DB-scanning tool for development. ### T-1129 — D-254 decision record (design pass) Full architecture record in governance/decisions/architecture.md: connection model (attach SR_PORT/9876 else spawn --port 0), ConnectionRole reader protocol with permitted-message matrix, wire-only data browser (no SQLite GDExtension, D-020; T-949 precedent), TradingReader future seam, loopback-only scope, save/load interaction seam (Phase 5+), and the SS3 amendment: **the companion is a generic implant HOST** — every registered implant app appears automatically; `available_in_companion: bool = true` on the app manifest is the opt-out for gameplay-dependent apps; the in-game implant ignores the flag entirely. ### T-1130 — reader connections (server, Oscar) - `ConnectionRole` (`Player | Reader`, `#[serde(default)]` = Player — old clients wire-compatible byte-for-byte; shaped for a future `TradingReader`). - `BridgeResource` rewritten 0-1 Player + 0-N Readers with `ConnectionId`; per-tick `accept_new_connections` loop replaces the single blocking accept (the old accept made any second connection hang the client and its disconnect kill the server). - Permitted-message matrix enforced: readers handshake + issue Atlas/StarMap/CityNames requests (responses connection-tagged, own requests only); `PlayerInput` from a reader = drop + strike (disconnect at 3); `ObserverSnapshot` has **no reader-facing send path at all** (structural, not filtered). - Shutdown scope: only a *Player* send failure flips `ServerRunning`. Reader-only servers (spawn-mode's common case — fixed from the original design during implementation) idle validly; reader disconnects never kill a session. - 6 new bridge_tcp integration tests + layer3 subprocess end-to-end; 79 tests exercised, full lib suite green. ### T-1132 — standalone shell + make atlas (client, Stig) - `atlas_standalone` scene/script: attach-or-spawn boot. **One real `connect_to_sim` attempt** — the originally-designed throwaway TCP probe was proven by live run to *kill* a pre-accept-loop server (probe consumed the blocking accept slot; server's handshake hit broken pipe and exited). Never abandon a connected socket. - Spawn path: `ServerProcess.start_with_pipe` (new method, existing API untouched) + `LISTENING:{port}` stdout parse, real systems.db, no --test-mode. - Reader role end to end: `encode_startup_message` optional role param (empty omits the key — byte-identical output for every existing caller, matching serde default); client suppresses `RequestAllSettings` and `AutoPause`/`AutoResume` auto-sends for readers (each would burn a violation strike — endorsed by Oscar cross-review). - **Generic implant host (D-254 SS3):** shell instantiates ALL registered implant apps, filtered by `available_in_companion`; registry default parameter preserves in-game behavior byte-identically. Future implant apps extend the companion automatically. - Owned-server lifecycle: spawning companion kills its child on close; attached servers survive companion close (verified live). Known engine limitation documented in-code: raw SIGTERM bypasses Godot notifications entirely and orphans a spawned server — WM close paths verified clean repeatedly. Follow-up T-1136 (server-side parent-death watchdog) filed. ### Live verification (real builds, real display) Spawn-mode boots and renders the live star map (301 systems over the wire); body browse renders full orbital layouts; attach-mode connects first-try against an independent server; **two simultaneous readers** verified with independent lifecycles; zero orphan processes after every graceful close including `make atlas` itself. ### Out of scope (wave 2, already ticketed) T-1131 browse data proxy, T-1133 data browser UI, T-1134 seed picker, T-1135 character-spawn split. Tickets: T-1129, T-1130, T-1132
jpmschweitzer added 5 commits 2026-07-17 09:16:40 +02:00
Six sections: connection model (attach SR_PORT/9876 ~500ms else spawn --port 0 + LISTENING parse, auto-attach-else-spawn); Reader connection class (ConnectionRole on StartupMessage, serde-default Player; no character spawn, NO ObserverSnapshot, permitted-message matrix, drop-not-disconnect, 0-1 Player + 0-N Readers, Player-only shutdown-on-disconnect); app shell (dedicated atlas_standalone.tscn, trivial make atlas target); data browser (separate implant/browser app per Jeroen, six registry entities v1, WIRE-ONLY extending the T-949 precedent — no client SQLite); save/load seam (seed picker now, save picker slot Phase 5+); trading seam (TradingReader superset, idempotency tokens, loopback-only auth assumption recorded). Tyre (lead) + Oscar (connection sections, his activation). Validate + sync clean (388 records, 0 broken).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Future implant apps/screens automatically extend the external app; per-app manifest bool (default true) locks out gameplay-dependent apps; in-game implant ignores the flag. Jeroen's suggested name availableInAtlasApp recorded with the rename rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ConnectionRole (Player|Reader, serde-default Player for wire back-compat;
shaped for a future TradingReader) on StartupMessage. BridgeResource
rewritten as 0-1 Player + 0-N Readers with ConnectionId; per-tick
accept_new_connections loop replaces the single blocking accept (the
listener is cloned non-blocking into ConnectionListener). First
connection installs per startup.role — spawn-mode Readers are often the
only connection a server gets.

Permitted-message matrix: readers may handshake and issue
Atlas/StarMap/CityNames requests (responses connection-tagged, own
requests only); PlayerInput from a reader is dropped with a strike
(disconnect at 3); ObserverSnapshot has no reader-facing path at all.
Shutdown scope: only a PLAYER send failure flips ServerRunning —
reader-only servers idle with the snapshot queued; reader disconnects
never kill the session.

Deliberately out of scope, documented at the call site: character spawn
stays in monolithic world-setup (reader-first servers carry an inert
unpiloted PlayerCharacter); no total-reader-count cap (per-reader frame
cap only, loopback-only scope).

6 new bridge_tcp integration tests; layer3 subprocess test exercises the
accept-loop end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).

Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.

Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Review — PR #183 (atlas-companion-app) — Tyre (Technical Architect)

Verdict: REQUEST_CHANGES — one reachable, user-facing defect in the client close-interceptor. Server side (T-1130) is clean and merge-ready; the block is a single-line fix in atlas_standalone.gd.

Reviewed against D-254 (which I authored) with extra rigor on the implementation-vs-record fit. The five already-adjudicated deviations are all implemented cleanly and verified. Determinism (D-010), the accept-loop state machine, the BridgeResource shape, protocol evolution, and the generic-host seam all pass.


REQUEST_CHANGES

client/scripts/atlas_standalone.gd:106-108 — close-interceptor re-enters open_app synchronously inside close_app's emit; the re-open's state is then clobbered, soft-locking the Atlas to keyboard input.

Trace (companion, Atlas fullscreen, user presses M/Escape on the "reach" screen):

  1. atlas_app._handle_key(KEY_M)HudGroups.close_app() (atlas_app.gd:77/85).
  2. close_app() emits app_changed("implant/map", GAMEPLAY) at hud_groups.gd:131before it resets _active_app (line 132).
  3. That synchronous emit reaches _on_hud_app_changed, condition true, which calls HudGroups.open_app(FRONT_APP) re-entrantly. The re-open sets _active_app="implant/map", _active_mode=FULLSCREEN, group z=Z_FULLSCREEN, and emits app_changed(..., FULLSCREEN) (churning atlas_app's on_closeon_open in the same frame).
  4. Control returns to the outer close_app, which continues past its emit: _active_app="", _active_mode=GAMEPLAY (lines 132-133), then gameplay_occluded.emit(false).

End state: the Atlas node is left visible with group z at fullscreen, but the manager's authoritative state is _active_app="" — so HudGroups.is_app_active("implant/map") returns false. atlas_app._unhandled_key_input guards on exactly that (atlas_app.gd:64), so after this single M/Escape press the Atlas is on screen but dead to all keyboard input (M, Escape, arrows, Enter all no-op). The companion is soft-locked with no keyboard recovery.

This is the reentrancy hazard the design's §3 option (a) opened without closing: "re-open implant/map" was implemented as a synchronous in-signal call, and close_app completing last wins. test_atlas_standalone.gd:17-19 explicitly defers this path to "a live run" rather than a unit test — and a casual live smoke test ("does M leave the Atlas up? yes → no-op as designed") would plausibly not then check that keys still work.

Fix (one line): defer the re-open out of the signal frame so close_app fully settles first —

func _on_hud_app_changed(app_path: String, mode: int) -> void:
    if app_path == FRONT_APP and mode == HudGroups.Mode.GAMEPLAY:
        HudGroups.open_app.call_deferred(FRONT_APP)

close_app then runs to completion (_active_app=""), and the deferred open_app executes next from a top-level context, restoring _active_app="implant/map" / is_app_active()==true and re-arming input. Matches how the script already defers _boot (_boot.call_deferred() at line 92). D-254's §3 record text stays accurate — it describes option (a) at a level of detail that doesn't prescribe the sync-vs-deferred call, so no record amendment is needed, just the code fix.


Verified clean (no change requested)

  • Determinism (D-010): reader requests route to AtlasRequestBuffer/StarMapRequestBuffer/CityNamesRequestBuffer and are served (atlas/plugin.rs) from BodyWorldStateCache (derived), StarMapDataPath (file), and CityContextReaderResource (read-only DB). None mutate ECS sim state. serve_atlas_requests takes Res<SimRng> but only calls .seed() (reads the immutable seed, never advances the RNG) — no ResMut<SimRng>, no perturbation. Reader Vec<PlayerInput> frames are struck+dropped in receive_bridge_inputs, never reach InputQueue. reader_never_receives_observer_snapshot_while_player_streams proves the boundary with a direct socket read.
  • Accept-loop state machine: PendingConnection is non-blocking by construction (set non-blocking at new(), WouldBlockWaiting, EOF/UnexpectedEofFailed, malformed startup→Failed). Per-tick work is bounded (one listener.accept() + one poll per pending). First blocking accept_on in main.rs completes before the tick loop starts and before ConnectionListener is ever queried — no double-accept, no race; the try_clone shares the fd correctly. Pending set is technically unbounded, but this is loopback-only per D-254 §3's recorded threat model (non-loopback requires §6 auth first) — acceptable and consistent with the deferred reader-count cap (T-1135).
  • BridgeResource shape: 0-1 Player + 0-N Readers with ConnectionId; send_snapshot has no reader-facing counterpart (structural D-010 enforcement); Connection.role carried (dead today) precisely so §6's TradingReader widens by match-arm, not struct change. Second-Player rejected post-handshake with a clean drop(stream) — the right call (you can't know the role until startup arrives, so pre-handshake rejection is impossible; post-handshake clean disconnect is a well-defined, discoverable failure). second_player_attempt_is_cleanly_rejected covers it.
  • send_bridge_snapshot: correctly distinguishes no-Player-ever (early return, snapshot stays queued, not fatal) from Player-disconnected-mid-send (fatal, flips ServerRunning). Adjudicated deviation #2, clean.
  • Client boot ownership: SimBridge.server_path="" so SimBridge never spawns — the shell owns _server_process; _exit_treestop() plus ServerProcess NOTIFICATION_PREDELETE is a correct double safety net (not double-ownership). One real connect attempt, no throwaway probe (deviation #3) — the header doc's live-repro rationale is sound. instantiate_all before open_app ordering documented and matches hud.gd's contract.
  • Generic-host seam: available_in_companion: bool = true opt-out on the manifest + instantiate_all(parent, standalone=false) default is the minimal correct seam; in-game path is byte-identical (default arg), flagged-off apps are never instantiated so never reachable by app-path. A future app-switcher has what it needs (the manifest carries app_path, and filtered apps simply aren't in _instances).
  • Protocol evolution: role as #[serde(default)] follows D-192; old-client (no key) → Player, new-client Player → omits key → byte-identical (old_format_startup_message_decodes_as_player hand-encodes the pre-D-254 shape to prove it). Both skew directions are graceful (old server ignores unknown role; new server defaults absent role to Player). make game Player path traced end-to-end — unchanged. Reader path early-returns before RequestAllSettings/AutoPause (sim_bridge.gd:322, hud_groups.gd:75) to avoid a guaranteed role-violation on every companion boot — a good detail.
  • D-254 record accuracy: the SS3 amendment matches what shipped (available_in_companion, instantiate_all standalone filter, in-game ignores the flag, option-(a) close handling leaving atlas_app.gd unmodified — confirmed unmodified). No record drift.
  • Makefile atlas target: SR_LIVE=1, build-server build-client deps, no make stop orchestration (correct — the shell owns its own lifecycle). Clean.

Note: the coastline-invention files (coast_invention.rs, district_profile.rs, believability.json, aliveness_probe.rs, etc.) and the .pql/changelog deletions in git diff main.. are merge-base noise (the branch is behind main after PR #182 landed), not part of this PR — no planning data is being clobbered.

## Review — PR #183 (atlas-companion-app) — Tyre (Technical Architect) **Verdict: REQUEST_CHANGES** — one reachable, user-facing defect in the client close-interceptor. Server side (T-1130) is clean and merge-ready; the block is a single-line fix in `atlas_standalone.gd`. Reviewed against D-254 (which I authored) with extra rigor on the implementation-vs-record fit. The five already-adjudicated deviations are all implemented cleanly and verified. Determinism (D-010), the accept-loop state machine, the BridgeResource shape, protocol evolution, and the generic-host seam all pass. --- ### REQUEST_CHANGES **`client/scripts/atlas_standalone.gd:106-108` — close-interceptor re-enters `open_app` synchronously inside `close_app`'s emit; the re-open's state is then clobbered, soft-locking the Atlas to keyboard input.** Trace (companion, Atlas fullscreen, user presses M/Escape on the "reach" screen): 1. `atlas_app._handle_key(KEY_M)` → `HudGroups.close_app()` (`atlas_app.gd:77`/`85`). 2. `close_app()` emits `app_changed("implant/map", GAMEPLAY)` at `hud_groups.gd:131` — **before** it resets `_active_app` (line 132). 3. That synchronous emit reaches `_on_hud_app_changed`, condition true, which calls `HudGroups.open_app(FRONT_APP)` **re-entrantly**. The re-open sets `_active_app="implant/map"`, `_active_mode=FULLSCREEN`, group z=`Z_FULLSCREEN`, and emits `app_changed(..., FULLSCREEN)` (churning `atlas_app`'s `on_close`→`on_open` in the same frame). 4. Control returns to the outer `close_app`, which continues past its emit: `_active_app=""`, `_active_mode=GAMEPLAY` (lines 132-133), then `gameplay_occluded.emit(false)`. **End state:** the Atlas node is left visible with group z at fullscreen, but the manager's authoritative state is `_active_app=""` — so `HudGroups.is_app_active("implant/map")` returns **false**. `atlas_app._unhandled_key_input` guards on exactly that (`atlas_app.gd:64`), so after this single M/Escape press the Atlas is on screen but **dead to all keyboard input** (M, Escape, arrows, Enter all no-op). The companion is soft-locked with no keyboard recovery. This is the reentrancy hazard the design's §3 option (a) opened without closing: "re-open implant/map" was implemented as a synchronous in-signal call, and `close_app` completing last wins. `test_atlas_standalone.gd:17-19` explicitly defers this path to "a live run" rather than a unit test — and a casual live smoke test ("does M leave the Atlas up? yes → no-op as designed") would plausibly not then check that keys still work. **Fix (one line):** defer the re-open out of the signal frame so `close_app` fully settles first — ```gdscript func _on_hud_app_changed(app_path: String, mode: int) -> void: if app_path == FRONT_APP and mode == HudGroups.Mode.GAMEPLAY: HudGroups.open_app.call_deferred(FRONT_APP) ``` `close_app` then runs to completion (`_active_app=""`), and the deferred `open_app` executes next from a top-level context, restoring `_active_app="implant/map"` / `is_app_active()==true` and re-arming input. Matches how the script already defers `_boot` (`_boot.call_deferred()` at line 92). D-254's §3 record text stays accurate — it describes option (a) at a level of detail that doesn't prescribe the sync-vs-deferred call, so no record amendment is needed, just the code fix. --- ### Verified clean (no change requested) - **Determinism (D-010):** reader requests route to `AtlasRequestBuffer`/`StarMapRequestBuffer`/`CityNamesRequestBuffer` and are served (`atlas/plugin.rs`) from `BodyWorldStateCache` (derived), `StarMapDataPath` (file), and `CityContextReaderResource` (read-only DB). None mutate ECS sim state. `serve_atlas_requests` takes `Res<SimRng>` but only calls `.seed()` (reads the immutable seed, never advances the RNG) — no `ResMut<SimRng>`, no perturbation. Reader `Vec<PlayerInput>` frames are struck+dropped in `receive_bridge_inputs`, never reach `InputQueue`. `reader_never_receives_observer_snapshot_while_player_streams` proves the boundary with a direct socket read. - **Accept-loop state machine:** `PendingConnection` is non-blocking by construction (set non-blocking at `new()`, `WouldBlock`→`Waiting`, EOF/`UnexpectedEof`→`Failed`, malformed startup→`Failed`). Per-tick work is bounded (one `listener.accept()` + one poll per pending). First blocking `accept_on` in `main.rs` completes before the tick loop starts and before `ConnectionListener` is ever queried — no double-accept, no race; the `try_clone` shares the fd correctly. Pending set is technically unbounded, but this is loopback-only per D-254 §3's recorded threat model (non-loopback requires §6 auth first) — acceptable and consistent with the deferred reader-count cap (T-1135). - **BridgeResource shape:** 0-1 Player + 0-N Readers with `ConnectionId`; `send_snapshot` has no reader-facing counterpart (structural D-010 enforcement); `Connection.role` carried (dead today) precisely so §6's `TradingReader` widens by match-arm, not struct change. Second-Player rejected post-handshake with a clean `drop(stream)` — the right call (you can't know the role until startup arrives, so pre-handshake rejection is impossible; post-handshake clean disconnect is a well-defined, discoverable failure). `second_player_attempt_is_cleanly_rejected` covers it. - **send_bridge_snapshot:** correctly distinguishes no-Player-ever (early return, snapshot stays queued, not fatal) from Player-disconnected-mid-send (fatal, flips `ServerRunning`). Adjudicated deviation #2, clean. - **Client boot ownership:** `SimBridge.server_path=""` so SimBridge never spawns — the shell owns `_server_process`; `_exit_tree`→`stop()` plus `ServerProcess` `NOTIFICATION_PREDELETE` is a correct double safety net (not double-ownership). One real connect attempt, no throwaway probe (deviation #3) — the header doc's live-repro rationale is sound. `instantiate_all` before `open_app` ordering documented and matches `hud.gd`'s contract. - **Generic-host seam:** `available_in_companion: bool = true` opt-out on the manifest + `instantiate_all(parent, standalone=false)` default is the minimal correct seam; in-game path is byte-identical (default arg), flagged-off apps are never instantiated so never reachable by app-path. A future app-switcher has what it needs (the manifest carries `app_path`, and filtered apps simply aren't in `_instances`). - **Protocol evolution:** `role` as `#[serde(default)]` follows D-192; old-client (no key) → Player, new-client Player → omits key → byte-identical (`old_format_startup_message_decodes_as_player` hand-encodes the pre-D-254 shape to prove it). Both skew directions are graceful (old server ignores unknown `role`; new server defaults absent `role` to Player). `make game` Player path traced end-to-end — unchanged. Reader path early-returns before `RequestAllSettings`/`AutoPause` (`sim_bridge.gd:322`, `hud_groups.gd:75`) to avoid a guaranteed role-violation on every companion boot — a good detail. - **D-254 record accuracy:** the SS3 amendment matches what shipped (`available_in_companion`, `instantiate_all` standalone filter, in-game ignores the flag, option-(a) close handling leaving `atlas_app.gd` unmodified — confirmed unmodified). No record drift. - **Makefile `atlas` target:** `SR_LIVE=1`, `build-server build-client` deps, no `make stop` orchestration (correct — the shell owns its own lifecycle). Clean. Note: the coastline-invention files (`coast_invention.rs`, `district_profile.rs`, `believability.json`, `aliveness_probe.rs`, etc.) and the `.pql/changelog` deletions in `git diff main..` are merge-base noise (the branch is behind main after PR #182 landed), not part of this PR — no planning data is being clobbered.
jpmschweitzer added 1 commit 2026-07-17 09:31:13 +02:00
Tyre's finding: _on_hud_app_changed ran open_app synchronously from
INSIDE close_app's app_changed emit; close_app continues past the emit
and resets _active_app, clobbering the re-open — Atlas left visible
(z already raised) but is_app_active() false, so atlas_app's input
guard rejects every key: keyboard soft-lock after one M/Escape on the
reach screen. Fix: open_app.call_deferred, out of the signal frame.

Two regression tests pin the contract at the HudGroups level (no
server needed): the synchronous shape must keep getting clobbered and
the deferred shape must survive — if HudGroups emit semantics ever
change, both flag the contract shift for a deliberate look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Review: atlas-companion-app → main (type: code) — round 2 (fix round)

Hoshe (QA): APPROVE

Wire back-compat proven by a hand-encoded pre-D-254 message test (old_format_startup_message_decodes_as_player, types.rs:1179) and every existing encode_startup_message caller keeps byte-identical output. The permitted-message matrix is structurally enforced — send_snapshot/send_bridge_snapshot have no reader-reachable path at all; violation strikes disconnect at 3 per spec; ConnectionId tagging through atlas/plugin.rs is genuinely 1:1 in-order. Shutdown scope correctly Player-only, with "reader-only server" a documented valid idle. Client Reader suppressions (sim_bridge RequestAllSettings, hud_groups AutoPause/AutoResume) are guard clauses leaving the Player path untouched. Registry filter is a no-op when standalone=false, so the in-game call site cannot regress. 13/13 bridge_tcp + 14/14 gdUnit confirmed on targeted reruns; the bridge tests use wall-clock deadlines and pair negative assertions with positive controls.

# File Issue
none

Tyre (Architecture): APPROVE (round 1: REQUEST_CHANGES, 1 finding — fixed and confirmed)

Server side merge-ready on round 1: accept-loop per-tick bounded, determinism holds by construction (readers touch only derived caches / read-only DB, never SimRng or ECS sim state), Player-only snapshot targeting structural, all five adjudicated design deviations implemented cleanly, protocol evolution byte-compatible in both skew directions, D-254 record accurate to what shipped.

Round-1 finding (fixed in 9b5c11396): atlas_standalone.gd close-interceptor called open_app synchronously from inside close_app's app_changed emit; close_app then reset _active_app, clobbering the re-open — Atlas left visible but is_app_active() false, killing the input guard: keyboard soft-lock after one M/Escape on the reach screen. Fix: open_app.call_deferred out of the signal frame, clobber mechanism documented at the call site. Two regression tests pin the contract at the HudGroups level — the sync shape provably clobbers (tripwire if emit semantics change), the deferred shape survives with an intermediate not-yet-active assert that makes it a genuine proof rather than a timing-luck pass. Re-review verdict: CONFIRMED, APPROVE.

# File Issue Status
1 client/scripts/atlas_standalone.gd:106 Sync re-open clobbered by close_app state reset → keyboard soft-lock FIXED (9b5c11396) + 2 regression tests

Verdict: APPROVED

Runtime verification: gate ran full cargo suite + full gdUnit (2984 → 2986 with the regression tests) + clippy -D warnings on every push. Author-side live verification (T-1132 report): spawn-mode renders the live star map (301 systems over the wire), attach-mode first-try, two simultaneous readers with independent lifecycles, zero orphan processes on all WM close paths. Merge-path smoke: connection-handshake changes are covered by the layer3 subprocess end-to-end test (real server binary spawn) plus the live attach/spawn runs above; the in-game Player path is byte-identical on the wire (hand-encoded back-compat test) and behaviorally untouched (guard-clause-only client changes).

## Review: atlas-companion-app → main (type: code) — round 2 (fix round) ### Hoshe (QA): APPROVE Wire back-compat proven by a hand-encoded pre-D-254 message test (`old_format_startup_message_decodes_as_player`, types.rs:1179) and every existing `encode_startup_message` caller keeps byte-identical output. The permitted-message matrix is structurally enforced — `send_snapshot`/`send_bridge_snapshot` have no reader-reachable path at all; violation strikes disconnect at 3 per spec; `ConnectionId` tagging through atlas/plugin.rs is genuinely 1:1 in-order. Shutdown scope correctly Player-only, with "reader-only server" a documented valid idle. Client Reader suppressions (sim_bridge RequestAllSettings, hud_groups AutoPause/AutoResume) are guard clauses leaving the Player path untouched. Registry filter is a no-op when `standalone=false`, so the in-game call site cannot regress. 13/13 bridge_tcp + 14/14 gdUnit confirmed on targeted reruns; the bridge tests use wall-clock deadlines and pair negative assertions with positive controls. | # | File | Issue | |---|------|-------| | — | — | none | ### Tyre (Architecture): APPROVE (round 1: REQUEST_CHANGES, 1 finding — fixed and confirmed) Server side merge-ready on round 1: accept-loop per-tick bounded, determinism holds by construction (readers touch only derived caches / read-only DB, never SimRng or ECS sim state), Player-only snapshot targeting structural, all five adjudicated design deviations implemented cleanly, protocol evolution byte-compatible in both skew directions, D-254 record accurate to what shipped. **Round-1 finding (fixed in 9b5c11396):** `atlas_standalone.gd` close-interceptor called `open_app` synchronously from inside `close_app`'s `app_changed` emit; `close_app` then reset `_active_app`, clobbering the re-open — Atlas left visible but `is_app_active()` false, killing the input guard: **keyboard soft-lock after one M/Escape on the reach screen**. Fix: `open_app.call_deferred` out of the signal frame, clobber mechanism documented at the call site. Two regression tests pin the contract at the HudGroups level — the sync shape provably clobbers (tripwire if emit semantics change), the deferred shape survives with an intermediate not-yet-active assert that makes it a genuine proof rather than a timing-luck pass. **Re-review verdict: CONFIRMED, APPROVE.** | # | File | Issue | Status | |---|------|-------|--------| | 1 | client/scripts/atlas_standalone.gd:106 | Sync re-open clobbered by close_app state reset → keyboard soft-lock | FIXED (9b5c11396) + 2 regression tests | ### Verdict: APPROVED Runtime verification: gate ran full cargo suite + full gdUnit (2984 → 2986 with the regression tests) + clippy -D warnings on every push. Author-side live verification (T-1132 report): spawn-mode renders the live star map (301 systems over the wire), attach-mode first-try, two simultaneous readers with independent lifecycles, zero orphan processes on all WM close paths. Merge-path smoke: connection-handshake changes are covered by the layer3 subprocess end-to-end test (real server binary spawn) plus the live attach/spawn runs above; the in-game Player path is byte-identical on the wire (hand-encoded back-compat test) and behaviorally untouched (guard-clause-only client changes).
jpmschweitzer closed this pull request 2026-07-17 09:32:13 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jpmschweitzer/settled-reach#183