docs(modules): the ingest walk, and the two-writer trap it walked into

The module half exercised against the live rig rather than only its own tests:
the working tree installed into a running core, pointed at the workstation
sidecar with a real Rust server behind it. The fresh-install path logged
`cursor started at the feed tail`, a console banid/unban arrived as two
rust_events rows carrying the wipe id, and the wipe row was created by being
mentioned rather than by any "a wipe started" call.

Then the boundary on real rows: with both ban events in the table, the public
events route answered {"events":[]} — and answered the same when asked for
player.banned BY NAME.

The trap is worth more than the proof. The state row read as online=1 with
hostname=NULL, a shape no code path writes. Two cores shared one database: a
second instance still held the phase 2 release in memory, spoke protocol 1, was
refused 409, and rewrote the row every thirty seconds. Nothing was wrong with
either. What made it look like a defect was two SELECTs in two round trips
assuming the state did not move between them — the same mistake the Pterodactyl
work recorded, and a single atomic read caught the coherent row alternating with
the other writer's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-16 08:52:48 -05:00
parent 30e72adfcf
commit f80b9f95c8

View File

@@ -2337,6 +2337,9 @@ either a coincidence or a property of login paths.)
| `rg.hooks` answers on both frameworks | Oxide and Carbon, identical output shape | ✅ | | `rg.hooks` answers on both frameworks | Oxide and Carbon, identical output shape | ✅ |
| 44 sidecar tests, 95 module server tests, 20 client tests, every guard | locally, and now in CI on both repos | ✅ | | 44 sidecar tests, 95 module server tests, 20 client tests, every guard | locally, and now in CI on both repos | ✅ |
| The module's route manifest against a **real core** at the pinned ref | 10 routes, all documented, none of core's moved | ✅ | | The module's route manifest against a **real core** at the pinned ref | 10 routes, all documented, none of core's moved | ✅ |
| **The module ingests a live game** | the working tree installed into a running core, pointed at the workstation sidecar: `cursor started at the feed tail`, then a console `banid`/`unban` arrived as two `rust_events` rows with their wipe id, cursor advanced | ✅ |
| **The allowlist holds against real rows** | with both ban events in the table, `GET /public/rust/servers/:id/events` answered `{"events":[]}` — **and answered the same when asked for `player.banned` by name** | ✅ |
| A wipe row is created by being mentioned | `w-20260915T195817Z` appeared in `rust_wipes` from the first frame carrying it, with no "a wipe started" call anywhere | ✅ |
**One finding about the rigs rather than the code:** the panel rigs cannot reach a sidecar running on **One finding about the rigs rather than the code:** the panel rigs cannot reach a sidecar running on
the workstation, because Windows Firewall holds two program-scoped **Block** rules for the workstation, because Windows Firewall holds two program-scoped **Block** rules for
@@ -2371,6 +2374,31 @@ boards and the classification are proven on both frameworks; the player half of
built, reviewed against the hook documentation, and unmeasured.** That is written here rather than built, reviewed against the hook documentation, and unmeasured.** That is written here rather than
in a commit message because it is the kind of thing a later phase will want to know it inherited. in a commit message because it is the kind of thing a later phase will want to know it inherited.
### 16.7b The ingest walk, and the trap it walked into
The module half was exercised against the live rig rather than only against its own tests: the
working tree installed into a running core, pointed at the workstation sidecar with a real Rust
server behind it. It logged `cursor started at the feed tail {at: 1}` — the fresh-install path,
starting at the end rather than replaying — and a console `banid`/`unban` pair then arrived as two
`rust_events` rows carrying the wipe id, with the cursor advancing to 3 and `events_seen` at 2.
Then the boundary, on real rows rather than fixtures: with both ban events sitting in the table, the
public events route answered `{"events":[]}`, **and answered the same when asked for
`player.banned` by name**. That is the difference between a filter and a refusal — the kinds are, as
far as a public caller is concerned, not there.
**The trap, and it is the same one the Pterodactyl work recorded.** The server-state row read as
`online=1` with `hostname=NULL` and `protocol=NULL`, which is a shape no code path writes. The cause
was two cores sharing one database: a second instance left running from earlier in the day still
holds the **phase 2 release** in memory, speaks protocol 1, is refused `409` by a protocol 2 sidecar,
and writes the row back as unreachable every thirty seconds. Nothing was wrong with either of them.
What made it *look* like a defect was the measurement: two `SELECT`s in two round trips, assuming
the state did not move between them. A single atomic read caught the coherent row a moment later —
`online: 1, protocol: 2, hostname: Test Server` — alternating with the other writer's.
**A differential diagnosis across two calls is only valid if nothing else holds the controls**, and
on a shared rig something usually does.
### 16.8 Smaller things worth keeping ### 16.8 Smaller things worth keeping
- **`rg.hooks` collides with `RGProbe`**, the phase-0 rig plugin, which registered the same console - **`rg.hooks` collides with `RGProbe`**, the phase-0 rig plugin, which registered the same console