# EtherGold (EG) version changelog

Format: `MajorFeature.MinorFeature.MajorFix.MinorFix` (product BOM).
Python tip = `EG_VERSION`. **uartd/inject must equal the tip** (`update.sh` rebuilds
when binary ≠ tip; `session_start` fails `*_ok` on lag). XR driver may still fall in
`[EG_COMPONENT_MIN_VERSION, EG_COMPONENT_MAX_VERSION]`.

## 2026-08-25 — collector User=topbox drop-in (still EG **4.0.0.6**)

**Listen-stock leftovers ran `ethergold-collector` as root**, so
`~/EtherGold/logs/supervisor.log` was root-owned and `log_offload.sh`
(topbox) aborted. `install_uartd_persist_dropin.sh` now also writes
`User=topbox` (same drop-in as `KillMode=process`), chowns that logs dir,
and restarts the collector **only when MainPID is not topbox**. Promote,
rollback, and `log_offload.sh` all run it. Idempotent. Does **not** bump
`EG_VERSION`.

## 2026-08-25 — HTTP `ethergold-http-2026-08-25-f` (still EG **4.0.0.6**)

**Production log PUT uses curl + Basic auth.** Debian wput is FTP-only.
`log_offload.sh` does `curl -T` with `~/EtherGold/sync/wput.env`
(`WPUT_USER` / `WPUT_PASSWORD`, user **SGVIUpload**). Anonymous GET/OTA
unchanged. Does **not** bump `EG_VERSION`.

## 2026-08-25 — HTTP `ethergold-http-2026-08-25-e` (still EG **4.0.0.6**)

**Machine move updates Enrollment.csv.** Identity Apply upserts this cabinet’s
Asset/Location/Path row in `Enrollment.csv` and `Enrollment.local.csv` (overlay
still wins after an OTA pull of an older catalog). The Pi does not publish
Dropbox SGVI. Does **not** bump `EG_VERSION`.

## 2026-08-25 — HTTP `ethergold-http-2026-08-25-d` (still EG **4.0.0.6**)

**Identity Grab GAMESTATS.** `/config` P3 panel has a button that POSTs
`/EtherGold/api/identity/p3-grab` with `force` (re-read even if TID is already
filled). Still idle-window only — does **not** GAMESTATS while
`current-game.json` shows recent play. Does **not** bump `EG_VERSION`.

## 2026-08-25 — HTTP `ethergold-http-2026-08-25-c` (still EG **4.0.0.6**)

**Identity read-only vs editable.** `/config` groups TID, machine number, and Mode in a dashed “From P3 GAMESTATS” panel. CSS `:read-only` is the common treatment (dashed border, muted fill, READ-ONLY on the label). Operator fields keep a solid border and gold focus. Does **not** bump `EG_VERSION`.

## 2026-08-25 — HTTP `ethergold-http-2026-08-25-b` (still EG **4.0.0.6**)

**Identity Location + log upload.** `/config` Identity card: Enrollment.csv
Location dropdown and **Upload logs to pog-atlas** (default on). Apply stamps
DF `config.ini` and, for a new asset at a known site, `Enrollment.local.csv`.
**wput** to `:51709` `{Path}/Logs/` or `/orphan/{asset}/` if the asset is set
but not enrolled. Unset asset skips IIS. rclone is not the log client.
Does **not** bump `EG_VERSION` / uartd.

**No pre-boot jackpot CSV.** Listen and `ethergold-http` unlink `progressives.csv`,
`progressives-sync.json`, and `current-game.json` when those files were written
before this OS boot. Lucky Street power-up had shown SuperBall at office ~$5k.
Do **not** bind or redirect port 80 — HDMI shows the IP; `:31234` stays unpublished.

## 4.0.0.6 — 2026-08-24 — sgvi-ota

**Promote restarts ethergold-http.** After Staging→live rsync, `promote_staging.sh`
runs `ota_restart_ethergold_http` (`sudo -n systemctl restart ethergold-http`).
Does **not** restart `ethergold-collector`. `SKIP_ETHERGOLD_HTTP=1` skips.
Rollback does the same. Prevents new field HTML from talking to a stale HTTP
process (login session vs Basic). First stamp with this OTA HTTP restart.

## 4.0.0.5 — 2026-08-24 — sgvi-ota

**Login return URL.** Unauthenticated `/EtherGold/config` and `/EtherGold/status`
redirect to `/?next=<path>`. After Sign in, `/` navigates back to that
allowlisted path (`/EtherGold/config`, `/config`, `/EtherGold/status`, `/status`).

**HTTP session dies with the process.** Sign in mints an in-memory UI session
(`HTTP_BUILD` `ethergold-http-2026-08-24-a`). Stored Basic is not enough after
an HTTP restart or Pi reboot. Open config/status/home tabs watch `/health`
`boot_id` and bounce to login when it changes. APIs reject stale session
headers with `session_stale` (no browser Basic dialog).

## 4.0.0.4 — 2026-08-24 — sgvi-ota

**Promote applies uartd persist drop-in.** After Staging→live rsync,
`promote_staging.sh` runs `sync/install_uartd_persist_dropin.sh`: writes
`/etc/systemd/system/ethergold-collector.service.d/uartd-persist.conf`
(`KillMode=process`, persist env) and `daemon-reload`. Does **not** restart
`ethergold-collector`. Needs `sudo -n`. `SKIP_UARTD_PERSIST_DROPIN=1` skips.
`hat_listen.sh` / listen supervisor default persist; `uartd_common.sh` will not
SIGKILL uartd unless `ETHERGOLD_UARTD_ALLOW_SIGKILL=1`. Rollback leaves the
drop-in in place. First field stamp with this OTA systemd path.

## 4.0.0.3 — 2026-08-23 — sgvi-ota

**Check for Updates.** `/EtherGold/config` (also `/config`) button rclone-pulls
SGVI into Staging only — no promote, no listen restart. POST
`/EtherGold/api/dyzineframe/check-updates` and
`/EtherGold/api/ethergold/check-updates`.

**HTTP_BUILD `ethergold-http-2026-08-23-f`:** Check for Updates greys OTA buttons
then re-enables them in `finally` (DF Promote comes back as soon as the DF
pull finishes; EG rclone no longer holds the DF row). Status refresh runs
after buttons are live again. 210 s fetch abort so a hung rclone cannot
leave the page stuck.

**Field UI shell (`HTTP_BUILD` `ethergold-http-2026-08-23-e`):** `/` is login, then
Home tiles for Config and Dashboard. JSON health moved to `/health`. Default
login user SGVI (password in `field_config.py` / `field-ui.auth`, aligned to
the field PSK already used by `leave_ap_join_wifi.sh`). `/config` loads
`field-config.json` into the form. **Apply** (above Software OTA) writes
`/etc/ethergold` env files with no reboot and no listen/collector restart.
TopBox profile `listen_ohd` is DyzineFrame TBX 1920×1080 (machine hooks) **and**
HTTP :31234 progressives (Win11 OHD can consume the same feed). New
`listen_ohd_server` is that same HTTP path with no local display.

## 4.0.0.2 — 2026-08-23 — sgvi-ota

**Field publish.** Same OTA machinery as 4.0.0.1. 4.0.0.1 stayed LAN-only on the
St. Thomas Pis so field SGVI would not still be 16-c when timers come back.
First `Publish-Sgvi.ps1 -Channel field` stamp for this train.

## 4.0.0.1 — 2026-08-23 — sgvi-ota

**Major:** Field software OTA is rclone from Dropbox **SGVI** (not pog-atlas.com:51709).
Chris publishes with `Publish-Sgvi.ps1 -Channel field` only. Pis hourly-stage
`~/EtherGold-Staging`, promote at 03:00 America/St_Thomas (or `/config` on that
Pi). `ROLLBACK.json` applies once by id; promote-hold blocks re-applying the
broken stamp. Listen restart is numeric python PIDs — never `pkill -f`, never
`sudo reboot`, UART `fuser` off unless `REBUILD_UART=1`.

Includes **3.1.0.7** pog-updt-0a-pass.

## 2026-08-22 — listen-stock keeps live Wi-Fi; current-game IPC on

**Stock:** `prepare_listen_stock.sh --keep-office-wifi` no longer deletes POGCOM
(or the active wlan0 link). `--strip-wifi` still wipes all SSIDs. Env stamp includes
`ETHERGOLD_CURRENT_GAME_IPC=write`. `SKIP_DF_SEED=1` skips Dropbox DF when LAN-seeding.

## 2026-08-21 — listen writes current-game.json

**Listen:** `pog422_listen.py` publishes `~/EtherGold/current-game.json` from UART
gameplay (same `CurrentGamePublisher` as the bridge). DyzineFrame’s watcher can
then `sds_handler()` after Heltec flash / SDS-gap. Collector env defaults
`ETHERGOLD_CURRENT_GAME_IPC=write`. Optional `ETHERGOLD_CURRENT_GAME_TID` filters
one cabinet on a bank tap (otherwise last mapped wager on this HAT).

## 2026-08-21 — listen-stock boots collector

**Stock:** `prepare_listen_stock.sh` enables `ethergold-collector` + `ethergold-http`
on boot (`POG422_UARTD_INJECT=0`). It no longer leaves `/var/lib/ethergold/disabled`
or waits for site apply. `install_collector_service.sh` merges env (keeps inject=0 /
CHANNEL=both).

## 3.1.0.7 — 2026-08-16 — pog-updt-0a-pass

**Fix:** Master **does** answer T340+ `0x02` (POG_UPDT). The reply is **`0x0A` @ 82**
(`stom_update`) — same opcode as the meter poll. Hub flooded it; EDGE spokes
**stashed it as `0a_latest_wins`** and never UART-injected it. Copper already has
that frame on the wire. `0x0A` is now STOM-priority (never game-gate defer).

Pos5 must run this rev (not only the hub). Includes **3.1.0.6** t340-cgs-config-pass.

## 3.1.0.6 — 2026-08-16 — t340-cgs-config-pass

**Fix:** T340+ CGS / Transfer Master Config must reach Master UART (copper would).

- Spoke **`0x02`** (and `0x01`/`0x04`) was ETH'd but hub **deferred inject** while any
  local close (`await_local_0d`) was open — and **dropped** under defer queue cap.
  Peer gameplay was allowed; config was not.
- Hub **unknown-TID** UART→ETH of Master **`0x09`/`0x0E`** used `[]` (send nowhere).
  Unmapped STOM now floods; mapped still unicasts.
- STOM is keep-score 95 if still deferred; stom inject is no longer gated off during
  local close (synth 0x0D stays P0).

Includes **3.1.0.5** spoke-stale-reap.

## 3.1.0.5 — 2026-08-16 — spoke-stale-reap

**Goal:** WLAN star hub drops dead / swapped spokes without a reboot.

- Hub reaps WireGuard peers with no handshake for **`EDGE_SPOKE_STALE_S`** (default **180s**).
- Removes `wg` peer + `eg-sNN` gretap, hot-drops the ETH port (`DirectionalEthRouter.remove_port`), updates `.active_spoke_gretaps`.
- Replacement board with the same preferred spoke id steals the slot once the old handshake is stale.
- Set `EDGE_SPOKE_STALE_S=0` to disable. Events: `hub_peer_dropped`, `hub_eth_port_removed`.

Includes **3.1.0.4** pogcc-com-update-sync.

## 3.1.0.4 — 2026-08-12 — pogcc-com-update-sync

**Goal:** Idempotent bank roll via `update.sh`.

- `install_pogcc_com_service.sh` modes: `auto` (default), `--enable-now`, `--stop`, `--unit-only`
- `auto`: refresh unit; enable only if DF `pogcc_com_owner=ethergold`; else stop+disable
- `EtherGold/sync/update.sh` + `DyzineFrame/update.sh` call auto sync (`SKIP_POGCC_COM=1` to skip)
- DF update ensures `pogcc_com_owner` key exists (default `dyzineframe`)
- **2026-08-16:** `commission_prod_boot.sh` also installs **`ethergold-http`** (`:31234`) and `loginctl linger` so Production boot is automatic after commission (DF still starts from desktop autostart, not SSH).

Includes **3.1.0.3** pogcc-com-df-local-http.

## 3.1.0.3 — 2026-08-12 — pogcc-com-df-local-http

**Fix:** On office/collector Pis, `ethergold-http` owns `:31234` and DF skips Bottle —
pogcc-com `/flag` + `/pogcc/hb-state` 404'd. Unit now targets
`http://127.0.0.1:31235`; DF **2026-08-12-b** binds that localhost port when
`pogcc_com_owner=ethergold`.

Includes **3.1.0.2** pogcc-com-runtime-dir.

## 3.1.0.2 — 2026-08-12 — pogcc-com-runtime-dir

**Fix:** `ethergold-pogcc-com.service` crash-looped (`PermissionError` on `/run/ethergold`).
Add `RuntimeDirectory=ethergold`; status write falls back to `~/EtherGold/status/`.

Includes **3.1.0.1** pogcc-com-service.

## 3.1.0.1 — 2026-08-12 — pogcc-com-service

**Goal:** EtherGold owns Heltec USB CDC + P3 GAMESTATS for POG-CC COM (canary).
MinorFeature bump: new `ethergold-pogcc-com` ownership path (DF becomes display-only when armed).

- `pogcc_com_service.py` / `pogcc_com_ports.py` / `ethergold-pogcc-com.service`
- Install: `bash install_pogcc_com_service.sh` (after DF `pogcc_com_owner=ethergold`)
- Resolves Heltec by USB VID `303a` (prefers last ACM if dual); P3 via `/dev/pogcc`
- Writes boot-arm + telemetry; `/sds` → current-game.json; `/flag` → DF HTTP
- Status: `/run/ethergold/pogcc-com.status`
- Pos5 canary: `POS5_POGCC_COM_CUTOVER.txt`

Includes **3.0.2.14** df-except-eg-mask.

## 3.0.2.14 — 2026-08-10 — df-except-eg-mask

**Goal:** Exclusive DF pin to core **2** (3.0.2.13) starved fireworks — sub‑1 FPS.

**Fix:** DyzineFrame uses **`OHD_CPU_DF_SET=0-2`** (all cores except EG). EtherGold
stays exclusive on core **3**. Live `update.sh` re-pins DF to the multi-core set.

Includes **3.0.2.13** df-live-cpu-pin.

## 3.0.2.13 — 2026-08-10 — df-live-cpu-pin

**Goal:** After tip install, live DyzineFrame was still free on cores 0–3 (old
supervisor, not taskset-wrapped) while EG was pinned to core 3 — DF could still
share EG’s core.

**Fix (`install_cpu_affinity.sh`, driven by `sync/update.sh`):**

1. After drop-ins/IRQ oneshot, **re-pin live** `pog422_uartd`, bridge Python,
   `dyzineframe_supervisor.py`, and `DyzineFrame.py` to the correct cores.
2. If `~/DyzineFrame/cpu_affinity.sh` is missing, write a minimal helper so
   next runit/supervisor start uses taskset.

Includes **3.0.2.12** txn-restart-local-x0.

## 3.0.2.12 — 2026-08-10 — txn-restart-local-x0

**Goal (pos5/witness51 MRT-clear thrash):** After hard MRT clear the glass restarts
the full transaction (often same `play_sequence` 40). EG left half-finished
await-x1 / late 41→0D from the abandoned attempt, so the restart was not clean
and the cabinet re-latched MRT.

**Fix:**

1. Local slave gameplay request (x0/40) **always** restarts the hand:
   abandons open close-wait **including same seq** (supersede was newer-only),
   force-closes same-seq trunk so `local_game_txn_open` can fire again, and
   runs existing same-seq close-done reopen (recovery immediate / ok settle).
2. **Hand generation:** each restart and missing-x1 abandon bumps a gen and
   unbinds prior x9; local x1 / uartd auto-close only seal when x9 gen matches
   current (stale late 41/0D after restart are dropped).

Includes **3.0.2.11** cpu-affinity-lane.

## 3.0.2.11 — 2026-08-10 — cpu-affinity-lane

**Goal:** Reduce post-x9 local **41** harvest misses (esp. pos5) and protect DyzineFrame
render FPS by giving each stack a preferred CPU on 4-core Pi 5.

**Phase A (process pin):**

- `cpu_layout.sh`: OS=0 NET=1 DF=2 EG=3 (override `OHD_CPU_*`; off: `OHD_CPU_ENABLE=0`).
- `uartd_common.sh` + `hat_bridge.sh`: `taskset -c $OHD_CPU_EG` for uartd + bridge Python.
- systemd drop-ins via `install_cpu_affinity.sh`: `CPUAffinity=` on `ethergold` /
  `ethergold-collector` (wired from `install_prod_service.sh` / collector / commission).
- **`sync/update.sh`** runs `install_cpu_affinity.sh` after each tip update (same as NM eth
  unmanaged). Opt out: `SKIP_CPU_AFFINITY=1`.

**Phase B (low risk IRQ/RPS):**

- `apply_irq_affinity.sh` + oneshot `ohd-irq-affinity.service`: USB xHCI etc. → EG core;
  eth/wlan prefer NET core; RPS on eth/gretap/wg/wlan → NET bitmask. Best-effort only.
  Also applied from `update.sh` via `install_cpu_affinity.sh`.

Includes **3.0.2.10** copper-x1-before-fanout.

## 3.0.2.10 — 2026-08-10 — copper-x1-before-fanout

**Goal (all10 jpot soak `0016` `$1107.80` 37BC):** Master did not reseed progressive after
a bank jackpot because hub treated **eth_rx of spoke 41** as “Master has the close.”
Multiport progressive fanout + `non_local_eth_x1_fanout` exclusive clear ran **before**
(and without) hub copper inject of winner 41 → Master re-TX elevated progressive; next
JP paid **$1114.20** (non-reset). Product intent was always: **winner unicast progressive
→ winner x1 on Master copper → then multiport peer fanout.**

**Fix:**

1. eth_rx of armed jpot 41 only stamps `bank_jpot_x1_eth_seen` + last_frame/queue time
   (`_note_bank_jpot_eth_x1_seen`). **Does not** multiport-flood or clear exclusive.
2. Multiport progressive flood only from copper seal: `_bank_jpot_x1_close_ok`
   (`inject_echo_ok` / `copper_packet_complete` / `inject_glued_0d`) →
   `_release_bank_jpot_eth_fanout`.
3. Retired living eth-only clear (`non_local_eth_x1_fanout` close_ok path is a no-op).
4. Soft peer-game open requires `fanout_released` (post-copper), not eth_seen alone.

Includes **3.0.2.9** prog-csv-snapshot meter logging.

## 3.0.2.9 — 2026-08-09 — prog-csv-snapshot

**Goal (all9 jpot soak `2113`):** progressive non-reset was only visible via climbing
x9 `progwin_cents` (e.g. D249 158980 → 177140). `progressive_0a_csv_update` logged
only game1/game16, so other meter slots and frozen reseeds were hard to judge from
events alone.

1. **`progressive_0a_csv_update`:** emit full **game1..game16** pennies whenever the
   0A set actually changes and CSV is rewritten.
2. **`progressives_csv_snapshot`:** once per ~60s from `capture_tick` (last 0A or
   on-disk CSV) with the same full game fields — stable trail even when values are
   frozen.
3. **`bank_jpot_award_x9_ok`:** include **`progwin_cents`** from the grant frame.

Includes **3.0.2.8** spoke-key-perm.

## 3.0.2.8 — 2026-08-09 — spoke-key-perm

**Goal (pos5 soak `1953`):** initial spoke EDGE connect often burned ~1–2 min on
`spoke_enroll_error` / pipe not ready when `/etc/ethergold/spoke/wg_private.key`
was root-owned. Directory probes could still look “writable” while the key file
itself denied write → enroll loop → MRT if glass played early.

**Fix:** `_keys_dir_ready_for_spoke` / `ensure_spoke_keypair` skip a tree that holds
non-RW key residue, then mint or use keys under `~/.local/share/ethergold/spoke`
(or configured `EDGE_SPOKE_KEYS_DIR`). No longer dies on EACCES against `/etc`.

Includes **3.0.2.7** concurrent eth queue work.

## 3.0.2.7 — 2026-08-09 — concurrent-eth-queue

**Goal (all10 soak `1827`):** multi-seat “retries” were mostly eth/txn queue
pressure, not soft pad or inject `attempt>1`.

1. **EDGE_TOPOLOGY=spoke:** skip eth→UART inject of **peer** gameplay
   (request/response/ack/0D). Spokes only need *local* TID game legs on copper;
   peer frames were stacking `transaction_defer` → `queue_cap` abandon (~1k on
   pos1/4). Progressive 0A / STOM / bank-jpot winner path unchanged.
2. **queue_cap eviction:** drop lowest-priority deferred first (foreign game
   before local x9/x1/0D; bank jpot last). Cap **64→128**.
3. **master_phase_diag hub:** stamp Phase A from **eth_rx x0** when copper x0
   is not local (hub inject path) so live A is usable under hub–spoke.

C string only rev-aligned (no hot-path C change vs 3.0.2.6).

## 3.0.2.6 — 2026-08-09 — soft-pad-master-phase

**Goal:** shave residual inject soft pad; **prove** Master Phase A on every hand.

**Soft pads (C):**

1. **pre_bulk SLV-IN:** `slv_in_phase_active` used GET_REG (`pog422_slv_in_live`)
   every inject — multi-ms USB. Prefer GPIO1 event cache (`slv_in_active`).
2. **pre_tx=0 path:** no `inject_poll_trace` / capture poll on zero sleep.
3. **HW_DEBUG copper:** PRE_DE / POST_OFF copper GET_REG only with
   `POG422_INJECT_FORENSICS=1` (not bare HW_DEBUG).
4. **DONE earlier:** after peel harvest, send `INJECT_DONE` *before* HS arm /
   post-x9 grace GPIO polls (was post-x1 soft work before Python unlocked).

**Master phase (Python):** emit `master_phase_diag`:

- `hub_40_sw` — eth→UART 40 inject wall (rpc, pre_bulk, job, air, de_hold)
- `copper_A_40_to_49` — copper gap + `master_beyond_hub40_us` (A minus hub inject)
- `copper_B_49_to_41` — local phase gap + full 40→41

Pass soak: `pre_bulk_us` p50 ≪ 1–2 ms; `master_beyond_hub40_us` shows where Master
sits beyond hub inject (~30 ms copper A still dominated by non-hub time).

## 3.0.2.5 — 2026-08-09 — inject-hotpath-peel-fix

**Soak `1549` (EG 3.0.2.4, hub3+spoke4):** functional OK (48:48 hands), no jpot.
But **no LNT win** — two regressions vs `0223`:

1. **`pre_bulk_us` still p50≈5988 µs** on every inject. Bench builds with
   `HW_DEBUG=1` forced `inject_forensics_full()` true → same pre-TX USB GET_REG
   pad. Copper timeline remains HW_DEBUG-gated separately.
2. **Spoke x9 peel worse:** `peel_first_byte_us` p50=**20063** (= full 20 ms
   first-byte gate); `uartd_post_burst` p50 **23.5 ms** vs **17.5 ms** on 0223.
   Cause: peel idle used `poll(POLLIN)` on the tty, but **dttty/wide ring RX does
   not wake POLLIN** — loop slept the full quiet window then read.

**.3.0.2.5:** forensics_full = env only (not HW_DEBUG); peel idle back to short
nanosleep (50 µs) + pull. Keep peel marks + cache preciseflags from .4.

## 3.0.2.4 — 2026-08-09 — inject-hotpath-peel

**Soak `0223` residual:** every eth→UART inject paid **`pre_bulk_us` p50 ≈ 6 ms**
with `idle_wait=0` — open-loop USB control forensics *before* DE (GET_RX_STATS,
RX FIFO count, GPIO_MODE, UART_FLOW). Spoke x9 peel residual (~17 ms post-burst)
is mostly Master wire RTT; first-byte/hard caps unchanged (.53 risk).

**.3.0.2.4:**

1. **inject_burst hot path:** skip pre-TX FIFO/regs probes (and post RX-FIFO
   sample) unless `POG422_INJECT_FORENSICS=1` or HW_DEBUG. Cache preciseflags
   after first confirm. Seed GPIO1 edge trace from the `require_idle` SLV-IN
   sample (no second probe).
2. **Peel:** `poll(POLLIN)` instead of 50 µs nanosleep spin; early-exit
   `post_de` when local x1 frame complete; keep 20 ms first-byte / 45 ms hard.
3. **IPC:** `INJECT_DONE` peel trailer (`peel_first_byte_us`, `peel_hit_us`,
   `peel_src`, `peel_x1_msg`) for soak marks; existing `X1_ACK_FRAME` still
   fires pre-DONE (event ACK for Master eth-forward — not a new open-loop).

Expect hub 40/41 inject wall **~−5–6 ms**; peel hit walls unchanged until Master
responds earlier.

## 3.0.2.3 — 2026-08-09 — jpot-hub-seal-fanout

**Field (soak `0202`):** after bank jpot on hub+spoke, spoke sealed clean but **hub**
held bank_jpot x1 priority for **30 s** (`close_miss hold_timeout`), **zero** peer-40
copper injects while EDGE still received eth 40, and multiport fanout blasted
**25** EDGE ports (`eth_tx 49`×26 + heavy `0D`). LNT “comms dead then horrible”.

**.3.0.2.3 fixes:**

1. **Award seal on hub:** stop discarding `award_x9_ok` on first arm; stamp
   `arm_progressive` from the progressive frame (result_x9 often lands ms before
   arm; discard → `seal_hold no_award_x9`).
2. **Non-local hold exit:** after eth x1 + multiport fanout release, hub
   (winner TID ≠ local_slave) clears exclusive (`non_local_eth_x1_fanout`) —
   copper seal remains for **local winner glass** only.
3. **Soft-open peer injects:** once every armed jpot has eth_x1 + fanout,
   `_should_defer` / hub allow resume peer x0/x9 (even if local seal pending).
4. **Fanout blast:** peer ports = TID-mapped spokes first, else carrier-up;
   skip multiport if no peers (winner already unicast). No more full EDGE map flood.

Known-good floor: **`3.0.2.1`** scoop; tip includes `.2.2` async AC.

## 3.0.2.2 — 2026-08-09 — inject-async-auto-close

**Fix:** spoke x9 `inject_rpc_scoop` ~21 ms was Python blocking under `_tx_lock`
for post-DONE **AUTO_CLOSE_DONE** (C still TX synth 0D). Soak `0137` (3.0.2.1):
scoop p50 21 ms on msg=49 while hub 40 scoop already ~0.1 ms.

**.3.0.2.2:**

1. After `INJECT_DONE`, still nonblock luxury scoop (`.2.1`); **no wait** for
   `AUTO_CLOSE_DONE` on success path (0D after DONE).
2. Miss AC still binds on inject result (uartd emits pre-DONE).
3. Success AC via capture/`on_auto_close_done` + FIFO `_ac_arm_q`.
4. `AUTO_CLOSE_DONE` promotes to capture queue (not discardable inject orphans).
5. Pending AC: no empty-echo reinject.

Expect spoke msg=49 `inject_rpc_scoop_us` ≪ 5 ms; full eth_rx49→done ~−20 ms.
Rollback / known-good floor: **`3.0.2.1`** inject-noblock-scoop.

## 3.0.2.1 — 2026-08-09 — inject-noblock-scoop

**Fix:** inject RPC wall ~110 ms was ~25 ms DONE + **~83 ms post-DONE scoop**
waiting on quiet-socket `settimeout(0.05)` (session `0121` inject-rpc-marks).

**.3.0.2.1:**

1. After `INJECT_DONE`, drain ECHO/FORENSICS/COPPER/capture with **timeout=0**
   until empty (uartd already emits luxury immediately after DONE).
2. Optional ≤5 ms arm only if forensics still missing (race).
3. Auto-close: targeted wait for `AUTO_CLOSE_DONE` only (5 ms slices), not a
   blanket 50–120 ms quiet scoop.

Expect hub peer-**40** `gpio_on→stage` / `inject_rpc_scoop_us` ≪ 10 ms typical.
Master copper RTT (~110 ms) unchanged.

Rollback: `3.0.1.58` inject-rpc-marks.

## 3.0.1.58 — 2026-08-09 — inject-rpc-marks

**Diag only:** after `.57` proved idle_wait=0 and C burst ~20 ms while Python wall
~112 ms, add marks that split **uartd pre-burst / burst / post-burst / job→DONE**
from **Python INJECT→DONE** and luxury scoop.

**.58 fields on `inject_stage_diag`:**

| Field | Source |
|-------|--------|
| `uartd_pre_burst_us` | handle_inject enter → `inject_burst` |
| `uartd_burst_wall_us` | `inject_burst` wall |
| `uartd_post_burst_us` | burst return → `INJECT_DONE` send |
| `uartd_job_to_done_us` | full worker until DONE |
| `inject_rpc_wait_us` | Python INJECT send → DONE recv |
| `inject_rpc_scoop_us` | DONE → client return (echo/forensics) |
| `inject_capture_ipc_n` | capture IPC drained while waiting |
| `rpc_minus_uartd_us` | Python wait beyond uartd job→DONE |
| `uartd_outside_burst_us` | uartd time not in inject_burst |

Rollback: `3.0.1.57` inject-stage-forensics.

## 3.0.1.57 — 2026-08-09 — inject-stage-forensics

**Diag only (no TX policy change):** hub eth→UART gameplay 0x40 shows ~113 ms wall
while DE hold ~11 ms — stage split instrumentation to prove pre-DE pad (idle wait).

**.57:**

1. C inject measures `idle_wait_elapsed_us`, `pre_bulk_us`, `bulk_wall_us`,
   `require_idle`, `idle_wait_budget_ms`, `idle_saw_busy` on every burst.
2. Fields on `INJECT_DONE` (trailer; short DONE still ok).
3. Bridge emits `inject_stage_diag` (events.jsonl) with
   `pre_de_pad_us` (= total − de_hold) and `pre_bulk_minus_idle_us`.

**Short soak focus:** hub + one spoke; confirm version/rebuild uartd; greps:
`inject_stage_diag` on msg=40.

Rollback: `3.0.1.56` jpot-lift-no-thrash.

## 3.0.1.56 — 2026-08-08 — jpot-lift-no-thrash

**Field:** after bank jackpot, console flooded with `bank_jpot_x1_priority_lift`
every main-loop poll.

**Cause:** `_lift_bank_jpot_x1_from_transaction_deferred` promoted foreign (and
other currently-deferrable) gameplay x1/0D out of Queue B; under local exclusive
they were immediately re-stashed → permanent lift/stashed thrash + log spam
through the 30 s arm / exclusive window.

**.56:**

1. Only lift frames that may inject **now** (skip if
   `_should_defer_transaction_inject` would bounce them).
2. Rate-limit `bank_jpot_x1_priority_lift` log (same cadence as inject_defer).
3. Keep `.55` hand-open-on-miss policy.

Rollback: `3.0.1.55` hand-open-on-miss.

## 3.0.1.55 — 2026-08-08 — hand-open-on-miss

**Policy:** empty x1 harvest = **hand still open**. Do not invent 0D / x1; let Master
re-49 and a real T340+ **41** complete the path (soak 1941: Python 0D after miss
then `x9_eth_inject_dedupe` / `seq_already_closed` killed glass).

**.55:**

1. **`auto_close_miss`:** never arm Python deal 0D (CGS or not). Emit
   `status_0d_auto_close_miss_hand_open`; clear Keno eth-inject age for that TID+seq.
2. **`mrt_x1_infer_*` disabled:** empty wait no longer `_mark_local_x1_pending(infer)`.
3. **While `await_x1` and not closed:** soft-dedupe does **not** drop Master re-49.
4. **Same-seq Master re-49** while still await_x1: refresh await clock + note
   `x9_retry_seen` (keep original peel path armed for second inject).
5. Keep peel timings of `.54` (no 20 ms hard-hunt); keep exclusive + DONE-before-0D.

Rollback: `3.0.1.54` restore-x1-peel.

## 3.0.1.54 — 2026-08-08 — restore-x1-peel

**Field:** `.53` (`x1-hunt-20ms`) → **continuous** glass MRT. Hard 20 ms peels +
no HS/post_de stack + 20 ms pending → mass `NO_X1` → Python 0D before real 41 /
Master retry can complete.

**.54 reverts .53 hunt policy; keeps .51 exclusive + .52 DONE-before-0D:**

1. Imm peel hard wall **45 ms** / first-byte **20 ms** again.
2. POST_DE 25 ms + secondary 15 ms; HS wait **40 ms**; pending **100 ms**.
3. Auto-close again stacks imm → post_de → HS on miss.
4. Python local FIFO grace **25 ms**; late_0D **150 ms**; x1-infer **25 ms**;
   AC scoop restored (0.12/0.20 s).
5. Synth 0D idle **25 ms** (.52).

Rollback: `3.0.1.52` if exclusive path also suspected (not the .53 regression).

## 3.0.1.53 — 2026-08-08 — x1-hunt-20ms

**FAILED IN FIELD** — continuous MRT. See **3.0.1.54**.

**Policy (rejected):** post-x9 **x1 insurance ≤ 20 ms.** If we miss x1, Master
re-sends 49 — do not invent 100+ ms stacked peels.

**.53 (reverted):**

1. C: imm peel hard wall **45 → 20 ms**; quiet activity gate **20 → 10 ms**.
2. C: POST_DE / secondary / HS wait / AUTO_CLOSE_PENDING / post-x9 grace **≤20 ms**.
3. C auto-close: **do not stack** full post_de + HS wait after imm peel.
4. C: synth 0D idle wait **≤20 ms**.
5. Python: local grace / late_0D / x1-infer **≤20 ms**; scoop shortened.

Rollback: `3.0.1.54` (or `3.0.1.52`).

## 3.0.1.52 — 2026-08-08 — x9-done-before-0d

**Soak `2026-08-08_1754-UTC_pos5-mrt-eg51-soak`:** every local 49 inject held
Python `inject_ioctl` **~114 ms** (p50) while wire TX of 49 was only **~17 ms**.
Eth **41** already arrived **~40 ms** into that window; remaining time was
**auto_close synth 0D** with **150 ms** HS-idle wait *before* INJECT_DONE.

**.52:**

1. After x9 TX + x1 peel: send **INJECT_DONE** (and luxury echo/forensics) **before**
   synth 0D — unlock eth→UART for concurrent traffic.
2. X1_ACK IPC still fires at harvest (before DONE) for Master eth-forward.
3. Synth 0D runs post-DONE with `AUTO_CLOSE_0D_IDLE_WAIT_MS` **150 → 25**.
4. AUTO_CLOSE_DONE (ok / fail) after 0D; Python scoop extended when auto_close armed.
5. Keep `.51` exclusive fence; no longer rely on long miss-defer.

Rollback: `3.0.1.51` local-game-txn-exclusive.

## 3.0.1.51 — 2026-08-08 — local-game-txn-exclusive

**Policy (operator):** once a *local slave* x0 (gameplay request) starts, treat the
hand as an exclusive **local game transaction** until 0D. Keep other eth→UART
traffic out — no “under load” excuse for barging progressive / foreign / stacked
same-seq 49 into the inject path. Stretching NO_X1 Python defer does not fix glass
MRT when CGS 40 stole the harvest window (soak 1634).

**.51:**

1. **`_local_game_txn_exclusive()`** from local trunk open (x0) through await_x1 /
   await_0d / synth 0D (spokes only; hub Master stays narrow 0D fence so peers do
   not freeze).
2. While exclusive: defer **all** eth→UART except this seat’s x9/x1/0D/skill,
   STOM, bank-jpot priority. Progressive and foreign always wait
   (`transaction_defer reason=local_game_txn_exclusive`).
3. **Second same-seq x9** after first inject: peel/hold (extends .17) so stacked
   eth 49 during first DE cannot land (1634).
4. **CGS during miss:** peer gameplay-request in inject RX → **no Python 0D**
   (`status_0d_auto_close_miss_cgs_no_synth`); do not “wait longer then close”.
5. **Revert** Python miss defer to **20 ms** (late AUTO_CLOSE only). Drop .50 long
   window as reliability strategy.
6. Events: `local_game_txn_open` / `local_game_txn_close`; local trunk stale bound
   `LOCAL_GAME_TXN_MAX_MS` (2 s).

Rollback: `3.0.1.49` if exclusive starves multi-TID on a mis-labeled hub seat.

## 3.0.1.50 — 2026-08-08 — late-x1-miss-defer

**Soak `2026-08-08_1634-UTC_pos5-mrt-eg49-soak`:** pos5 `.49` autoplay ~76 good
closes then glass MRT. **Not** reopen/late_0d: one `auto_close_miss` harvest=0
with concurrent CGS `40` during 49 inject; Python 0D closed ~100 ms later; true
local `41` arrived **~212 ms** after miss — already `seq_already_closed`. Autoplay
issued no further 40. Pos1/2/4 zero miss on same bank.

**.50:**

1. `STATUS_0D_PYTHON_FALLBACK_DEFER_MS` **20 → 250** ms (late x1 can win).
2. Concurrent gameplay-request in inject peer RX →
   `STATUS_0D_PYTHON_FALLBACK_DEFER_CGS_MS` **350** ms, reason
   `auto_close_miss_cgs`.
3. Late local x1 while miss-defer armed: clear ready timer, emit
   `status_0d_python_fallback_late_x1`, schedule close with x1 (not pure NO_X1).

Keep .49 recovery/ok reopen. Rollback: 3.0.1.49 if miss defer starves true misses
into glass MRT without close.

## 3.0.1.49 — 2026-08-08 — reopen-recovery-ok

**Field:** pos5 continuous MRT again after `.48` settle-only reopen (2500 ms on
*all* closes). Glass re-Play after miss/close or product MRT stayed
`seq_already_closed` until long settle — same latch class as soak 1436.

**.49:**

1. Close quality **`ok` | `recovery`**: `auto_close_miss` Python fallback and
   `mrt_risk_late_0d_after_x1` stamp **recovery** → same-seq local wager reopens
   **immediately**.
2. Normal uartd good close stamps **ok** → reopen only after
   `MRT_HAND_REOPEN_MIN_AGE_MS` (**400 ms**, was 2500) — still blocks 1513 CGS
   ~1 ms re-wager → false late_0d.
3. Events: reopen `reason=same_seq_local_wager_recovery|settled`, quality field;
   skip still `too_soon_after_close` for ok path.

Rollback: `3.0.1.48` only if CGS-class late_0d returns under 400 ms settle.

## 3.0.1.48 — 2026-08-08 — reopen-settled-only

**Soak `2026-08-08_1513-UTC_pos5-mrt-after-eg347`:** on **.47**, pos5 advanced
play_sequences and still hit product MRT. Metrics: **8×** `mrt_risk_hand_reopen`
paired with **8×** `mrt_risk_late_0d_after_x1` (bound_ms=150). Pattern: good
uartd close → CGS same-seq re-wager ~1 ms later → reopen cleared close-done →
re-armed `await_0d` without a second 0D → classic late_0d. Continuous
`seq_already_closed` latch from 1436 is gone; reopen was too eager.

**.48:**

1. **`_mrt_risk_reopen_hand_on_local_wager` gated:** only after close-done age ≥
   `MRT_HAND_REOPEN_MIN_AGE_MS` (2500 ms) and not while open close-wait.
   Skips emit `mrt_risk_hand_reopen_skip` (`too_soon_after_close` /
   `open_close_wait`). Settle window still recovers 1436-class latch without reboot
   (glass clear + Play is multi-second).
2. Stamp `_mrt_risk_close_done_ns` with `_mrt_risk_mark_seq_closed`.

Also residual this soak (not fixed here): **2×** `status_0d_auto_close_miss`
(NO_X1); Master 40→49 RTT outliers to ~737 ms.

Rollback: `3.0.1.47` (only if continuous latch returns and skip age is wrong).

## 3.0.1.47 — 2026-08-08 — reopen-same-seq-close

**Soak `2026-08-08_1436-UTC_pos5-immediate-mrt`:** continuous glass MRT while EG
logged `seq_already_closed` / `already_closed` on every retry. Only one NO_X1
seed; glass stayed on play_sequence **2** forever; bridge reboot cleared the
close-done map and restored play. Pos1–4 healthy on same tip.

**.47:**

1. **`_mrt_risk_reopen_hand_on_local_wager`:** local same-seq gameplay request
   (`0x40` etc.) after that seq was marked closed → clear
   `_mrt_risk_close_done_seq` + local close-wait / synth_sent → emit
   `mrt_risk_hand_reopen`. Next eth→UART x9 can arm auto-close again
   (no Pi reboot required).
2. **Spoke WG keys:** `ensure_spoke_keys_dir` / robust `ensure_spoke_keypair`
   fall through from unreadable/unwritable `/etc/ethergold/spoke` to
   `~/.local/share/ethergold/spoke` (fixes `Permission denied: wg_private.key`
   enroll spam). Same pattern as hub peer store (3.0.1.30).

Rollback: `3.0.1.46`.

## 3.0.1.46 — 2026-08-07 — revert-deal-open-guard

**Soak `2026-08-08_0011-UTC_master-no-slave-cgs-eg345` (pos1–4, tip .45):**
Master T340+ saw **no slave data**; slaves stuck in **CGS**. Master eth_rx many
`0x40` but `transaction_defer` (game_transaction_gate + queue_cap) starved UART
inject; almost **no copper `packet_complete` 40** on Master. Root cause: .45
“serialize foreign 0x40 while any trunk open” — first open trunk never cleared
fast enough → peer wagers deferred forever/to cap → glass never saw wagers.

**.46 REVERT of .45 inject gates:**

1. Remove `_deal_open_guard_active` and same-TID re-wager defer under await_x9.
2. Remove serialize-foreign-0x40-while-any-trunk-open.
3. Restore pre-.45: gameplay requests (same-TID supersede + other TID 40) pass
   the transaction gate; soft `0x0A` still defers while trunk open.

`MASTER_DEAL_OPEN_GUARD_MS` remains in core as docs only (not wired).
MRT late first-deal (2258) is still open — do **not** re-land serialize without a
proven flush path.

Rollback: `3.0.1.44` (skip .45); operational tip is this rev after .45 field fail.

## 3.0.1.45 — 2026-08-07 — master-deal-open-guard ⚠️ FIELD FAIL

**MRT soak 2026-08-07_2258 (pos5 D249):** Master eth_rx40→uart49 ~276 ms on the
miss hand vs ~113 ms healthy. Inject queue wait for first D249 40 was ~0.4 ms —
not eth backlog. Same-seq CGS re-wager ~170 ms after first 40 was **re-injected
onto Master copper ~18 ms before a healthy first-deal 49 window**; 49 landed
~104 ms after the **second** 40 only. Glass had no x1 for the late first result.

**.45 Master deal-open guard (BAD — do not ship):**

1. **`MASTER_DEAL_OPEN_GUARD_MS` (140 ms):** while trunk open for a TID and x9 not
   yet marked, **defer same-TID gameplay re-wager eth→UART inject**.
2. **Serialize foreign new 0x40:** while any trunk is open, defer **other TID**
   gameplay requests.
3. x1 / 0D / STOM / P0 still never deferred.

**Field fail soak 2026-08-08_0011:** Master glass dark / slaves CGS — see **3.0.1.46**.

Rollback: `3.0.1.44` or deploy **3.0.1.46**.

## 3.0.1.44 — 2026-08-07 — x1-rx-pending-100

**NO_X1 MRT at pos5 18:16:** hard-RT auto-close miss (post_de filled with next
keno `0x40`); instant Python P0 raced C pending; peel exited early on complete
non-x1 frame.

**.44:**

1. **C `AUTO_CLOSE_PENDING_NS` 200 ms → 100 ms** (glass timeout territory).
2. **Imm x1 peel:** after DE→RX, hunt local ACK to hard_cap — do not stop on
   complete non-x1 frames; trust RxD over “quiet HS” frame-exit.
3. **Python miss fallback** defers **`STATUS_0D_PYTHON_FALLBACK_DEFER_MS` (20 ms)**
   for late `AUTO_CLOSE_DONE`; cancel if uartd closes first.
   Extra 0D is harmless to T340+; defer is for late C close, not multi-0D fear.

Rollback: `3.0.1.43`.

## 3.0.1.43 — 2026-08-07 — 0d-single-owner

**Soak 2026-08-07_1944 (pos5 MRT + dual 0D):** uartd auto-close succeeded
(`status_0d_synth_done source=uartd_auto`), then Python re-armed after
`_release_synth_sent` + `_queue_synth` bypass of the uartd gate and fired a
second full-forensics 0D. Second owner is wrong; MRT `seq_already_closed` noise.

**.43 single owner for deal close:**

1. **Python deal 0D never queues while uartd owns** — `_queue_synth` /
   `_try_synth_pending_for_round` / schedule all gate on
   `_python_may_deal_synth_close` (fallback only).
2. **After auto_done (synth_sent):** re-harvest of local x1 →
   `status_0d_synth_skip reason=already_closed` — no re-pending, no release of
   synth_sent that cleared the dual path.
3. **auto_close_miss only:** arm one Python P0 via
   `_arm_python_deal_close_fallback` + `_synth_retry_rks`.
4. Skill-grant close remains Python (C auto-close is deal path only).

Rollback: `3.0.1.42`.

## 3.0.1.42 — 2026-08-07 — jpot-seal-reopen-hand

**Unclaimed progressive JP14 soak (`logs` 2026-08-07 T15:12):** copper grant + fireworks
fired; winner T340+ never “noted” (seq closed by empty `41`); Master sealed-hold
on **pre-arm empty x1 inject_echo** twice; arm **hold_timeout** + STOM starve.

**.42 fixes (seal + hand reopen, keep .38 anti-steal):**

1. **Bank progressive arm reopens hand** for winner TID/seq: clear
   `mrt_risk_close_done_seq` + empty-close block so auto-close / MRT are not stuck
   `seq_already_closed`.
2. **Seal requires `award_x9_ok` AND post-arm x1** (not post-arm alone).
3. **Reinject stamps** `_bank_jpot_x1_eth_queued_ns = now`; only reinject when a
   **post-arm** last_frame exists (purge / refuse pre-arm empty as last_frame).
4. **Local progressive issue** injects front-of-queue at bank-jpot class while armed.
5. While bank-jpot arm (or award set): **native Master eth `0D` not blocked** by
   `status_0d_block_all` (synth remains backup).
6. Hold-timeout path still clears priority (no flood) so STOM recovers.

Rollback: `3.0.1.41`.

## 3.0.1.41 — 2026-08-07 — pogcc-telemetry-diag

When `pogcc` is null, machine-status includes `pogcc_telemetry_path`,
`pogcc_telemetry_present`, and `pogcc_hint`. status/ created 775+owned by topbox
so DyzineFrame can write pogcc-telemetry.json when bridge created the dir as root.

Rollback: `3.0.1.40`.

## 3.0.1.40 — 2026-08-07 — machine-status-json

**pog-atlas machine status file:** durable
`~/EtherGold/status/machine-status.json` (outside tip trees — survives
`update.sh` / code rsync). EtherGold merges:

- `status/pogcc-telemetry.json` — DyzineFrame from POG-CC `/telemetry` (~5s):
  four INA rails V/I/P, upper/lower temps °F, credit balance, game menu
- `progressives.csv` — all 1..16 pennies + game names
- `status/last-progressive-hit.json` — last bank progressive grant
  (UTC + game_index/name + progwin)

Refresh on 0x0A CSV updates, progressive hit, and bridged packet traffic ~5s.
Not under `EtherGold/sync/` package paths.

Rollback: `3.0.1.39`.

## 3.0.1.39 — 2026-08-07 — heltec-boot-arm-force-cg

**Gap-fill without replacing SDS:** after Heltec USB-up, DyzineFrame writes
`~/EtherGold/pogcc-boot-arm.json` from POG-CC `/hello/<fw>/<local_ms>/<utc_ms>`.
Bridge always runs current-game observe; continuous write still gated by
`ETHERGOLD_CURRENT_GAME_IPC`. When arm is present, the **next local-TID**
mapped gameplay **force-publishes** `current-game.json` even if index unchanged
(so DF inotify re-applies SuperBall after Heltec flash). Arm file cleared on
publish. SDS enter-game remains primary when live.

Rollback: `3.0.1.38`.

## 3.0.1.38 — 2026-08-06 — jpot-award-before-seal

**Stolen progressive (JP3 soak `2026-08-06_2040`):** Master sealed bank
progressive and reset game11 meters by eth→UART-injecting a **stale empty `41`**
that was already queued for the winner TID ~200 ms *before* the progressive
grant (`progwin>0`). Glass never showed hits / Jackpot Won; CGS followed.

- On first bank-jpot **arm**: purge pending/deferred/spoke **pre-arm x1** for that
  TID; stamp `arm_ns`; clear empty-close block.
- **Seal** (`inject_echo_ok` / copper) only for **post-arm** x1 evidence
  (`bank_jpot_seal_hold` if pre-arm/stale).
- Winner only: eth→UART inject of progressive grant; peers skip
  (`bank_jpot_peer_no_uart_inject`) — OHD via bank_wlan remains Master-side.
- Local **empty close** (x1 without recent result x9) blocks progressive award
  inject (`bank_jpot_empty_close_no_award`).
- **hold_timeout / max_reinject**: drop stashed eth fanout (no multiport flood).

Keeps 3.0.1.37 never soft-dedupe of local progressive grant.

Rollback: `3.0.1.37`.

## 3.0.1.37 — 2026-08-06 — jpot-award-never-dedupe

**Product law:** local-terminal bank progressive grant (`progwinval>0`) is never
`x9_eth_inject_dedupe`'d. Award inject is not optional.

Soak `2026-08-06_1744-UTC_pos1-5-jpot-reset-no-award`: cash SuperBall `0x49`
(progwin=0) inject stamped TID+seq; true jpot `0x49` (progwin $1631.50) ~292 ms
later hit **dedupe before bank-jpot arm** → pos4 no eth→UART award, while Master
copper-sealed x1 and **0A game11 reset** ($1631→$1000). Mirror dealkiller:
reset without Jackpot Won.

- Local + `is_bank_progressive_jackpot_frame` → never soft-dedupe (do not require
  arm already open — eth order is dedupe then arm).
- Peer progressive flood still age-dedupes; pending match ignores non-jpot cash
  for bank_jpot frames.
- Fix `bank_jpot_eth_fanout_release` TypeError: `router.port_names` is a method
  (`port_names()`) — was `'method' object is not iterable`.

Rollback: `3.0.1.36`.

## 3.0.1.36 — 2026-08-06 — jpot-seal-abort-window

`BANK_JPOT_X1_SEAL_MS` **250 ms** (was 10 000). Master progressive grant
aborts in ~**200 ms**; post-copper sealed reinject must cover only that race
(2nd/3rd Master `49` re-TX), not linger for seconds. Real path is still
exclusive until copper so the first x1 hits UART before / with the first retry.

Rollback: `3.0.1.35`.

## 3.0.1.35 — 2026-08-06 — jpot-copper-seal-reset

**Dealkiller:** Slave shows progressive win while Master 0A meter keeps
climbing (casino false advertising). Soak 2026-08-06_1651 SuperBall TID 37BC
`progwin=$1572.27`: Master **did** get eth x1 at **+48 ms**, but copper inject
echo only at **+172 ms** — *after* Master already re-TX progressive `49` at
**+163 ms**. EG 3.0.1.34 cleared exclusive arm on `eth_rx_x1`, so Master
progressive retries #2/#3 got **no** x1 reinject; arm #3 `hold_timeout` 30 s;
**game11 never reset** on copper 0A (OHD stayed $1572.xx).

Fix:
- **Copper seal only:** exclusive clear + `bank_jpot_x1_sealed` on
  `inject_echo_ok` / copper PC / glued 0D — **not** eth_rx.
- eth_rx still releases multiport fanout (`bank_jpot_x1_eth_seen`) for CGS.
- Retain last x1 frame for `BANK_JPOT_X1_SEAL_MS` after copper only (see 3.0.1.36
  for ms) so Master progressive re-TX can reinject without a second 30 s arm
  (`bank_jpot_x1_sealed_retry` / `master_progressive_retry_sealed`).

Rollback: `3.0.1.34`.

## 3.0.1.34 — 2026-08-06 — jpot-clear-on-x1-evidence

Master (and any arming seat) bank-jpot exclusive arm cleared when winner x1 is
**proven**, not only when copper `packet_complete` sees that x1 after inject.

Soak `2026-08-06_1258-UTC_pos1-5-jpot-hold-timeout-eval`: every Master arm hit
`hold_timeout` @ 30 s while winner seats `close_ok` in ~0.12 s. Master **did**
`eth_rx` + inject_echo **match** the x1 (`bank_jpot_x1_inject_done`); clear
used to require copper PC of peeled self-echo (never fires) → 30 s x0 inject
defer / CGS.

- Clear evidence: `eth_rx` of armed TID x1; inject echo `exact|prefix|embedded`
  or non-empty echo; copper PC (unchanged); glued Master `0D` after peel.
- `bank_jpot_x1_close_ok` gains `evidence=…`; clear reason matches.
- `bank_jpot_x1_bus_trail` when inject RX has peer payload after self-echo
  (expect glued Master 0D <3 ms when present — soak often had exact 12 B only).

Keeps 3.0.1.33 never-defer close legs. Rollback: `3.0.1.33`.

## 3.0.1.33 — 2026-08-05 — jpot-never-defer-close

**Policy:** never transaction-defer gameplay **x1** or **0x0D** (incl.
`status_0d_synth`) for any TID while bank progressive close is armed.

EG 3.0.1.27 exclusive path deferred *everything* except winner x1 + progressive
issue, so a non-winning slave's own close legs waited behind the jackpot window
→ true MRT (`x1` without timely `0x0D`) and post-jpot CGS for 1–2 plays even when
jackpot meters reset.

- `is_gameplay_close_leg`: any game ACK or status 0x0D / synth.
- `_is_bank_jpot_close_path_frame` / cancel / lift / hub allow: close legs pass.
- Soft progressive / peer x0 / non-jpot x9 may still yield during the arm window.
- Winner x1 remains highest bank-jpot priority; eth no-repeat (3.0.1.32) kept.

Rollback: `3.0.1.32` (jpot-eth-no-repeat).

## 3.0.1.32 — 2026-08-05 — jpot-eth-no-repeat

Fix: after bank progressive, multiport eth delivery was re-playing the **same
progwinval** on seats (and re-arming 30s exclusive close on every spoke) →
operator sees the same jackpot over and over on every game path.

- **eth_rx arm** bank-jpot x1 priority only when the **winner TID is local**
  (peers still inject progressive for bank bells; no 30s freeze-as-winner).
- **Post-x1 eth fanout** excludes the winner port (unicast hold already
  delivered the grant; no second inject + re-arm).
- **Eth inject dedupe** for bank-progressive re-floods (non-winner and post-
  clear winner); local winner **while still armed** still never-dedupes so
  Master progressive retries can re-inject for close.

Keeps 3.0.1.31 winner-unicast-until-x1. Rollback: `3.0.1.31` (jpot-defer-fanout)
or prod-safe `3.0.1.30` if defer-fanout still suspect.

## 3.0.1.31 — 2026-08-05 — jpot-defer-fanout

Bank progressive eth: **winner unicast first**, full multiport fanout only after
Master has the winner **x1**. Avoid out-of-order bus clog that stalls other seats'
SuperBall (and similar) **game requests** (operator "Contacting Game Server" / multi-
round abort) while progressive close is still open.

- Master `uart→eth` / edge relay of `progwinval>0` x9 (or skill grant): `only_ports`
  = winner TID gretap until release; stashed for deferred flood.
- **Release** on Master `eth_rx` of that TID's x1, copper `packet_complete` x1, or
  bank-jpot arm clear/miss (`bank_jpot_eth_fanout_release` / `_hold` events).
- SCR (Send Com Request Local Net Tests) bank flood path unchanged (not CGS).

Rollback: `3.0.1.30` (auto-edge-cgs-path).

## 3.0.1.30 — 2026-08-05 — auto-edge-cgs-path

Zero manual ops for star CGS: enroll + Master path recover without EDGE_HUB_IP or
chmod.

- **Spoke uart→eth:** unmapped TID no longer black-holes local x0 (`only_ports=
  None` → all local eth ports = hub uplink). Hub still uses `[]` for unknown
  (no multi-spoke flood on map miss).
- **Hub peer store:** `ensure_hub_state_dir` falls through to
  `~/.local/share/ethergold/hub` when `/etc/ethergold/hub` is not writable;
  merges any existing system `peers.json` for read.
- **Spoke enroll:** poll bank-wlan `EDGE_UPLINK_IP_LEARNED` (and env/WG endpoint)
  for life of bridge — no one-shot `spoke_enroll_skip no_hub_ip`.
- **Hub bank-wlan:** auto-publish `_guess_uplink_ip` as `hub_uplink_ip` so
  spokes learn without manual env.

Rollback: `3.0.1.29` (spoke-tx-arbiter).

## 3.0.1.29 — 2026-08-05 — spoke-tx-arbiter

Star hub inject + eth fanout: prioritize local gameplay with per-spoke buffers and
per-TID transaction FSM; copper stays one-at-a-time.

- **Eth:** TID is always on the wire; “unknown” means **TID→gretap map** missing.
  Unmapped gameplay never multi-port floods (`only_ports=[]`, log `tid_port_unknown`).
  Bank-wide only: progwin>0 x9, progressive **0A**, SCR.
- **Per-spoke inject queues** (`_spoke_queues`) + copper arbiter rebuild of `_pending`
  by score (P0 / bank jpot / open-tx required legs / local terminal; soft 0A yields).
- **TidTxFsm** per TID: `idle → await_x9 → await_x1 → await_0d → idle` (events
  `tid_tx_fsm`). Soft **0A** defers while any FSM open; 0A soft lane is latest-wins.
- Bank jpot exclusive close path retained (3.0.1.27/28).

Rollback: `3.0.1.28` (jpot-x1-copper-clear).

## 3.0.1.28 — 2026-08-04 — jpot-x1-copper-clear

Bank progressive close stays armed until Master **copper** `packet_complete` of the
winner x1 — not until eth→UART inject_done / self-echo alone.

- **Clear only on** copper gameplay ack PC for armed TID (`bank_jpot_x1_close_ok` +
  `bank_jpot_x1_priority_clear` reason=`copper_packet_complete`).
- **Do not clear** on `eth_to_uart_done` of bank-jpot x1; emit `bank_jpot_x1_inject_done`
  with echo_len / post_de_len / reinjects (self-echo alone never closes).
- **Reinject:** empty inject echo → queue `bank_jpot_x1_reinject`; Master progressive
  re-arm with stored frame → reinject (cap `BANK_JPOT_X1_MAX_REINJECT=12`).
- **Miss:** hold timeout or max reinjects → `bank_jpot_x1_close_miss` then clear
  (reason `hold_timeout` / `max_reinject`).
- Arm payload adds `rearm`; clear payload includes forensics fields.

Rollback: `3.0.1.27` (jpot-close-primary).

## 3.0.1.27 — 2026-08-04 — jpot-close-primary

Bank progressive jackpot close is exclusive primary until the winner’s x1 injects —
no IOP required. Closer to native: Master grant retries still get out; everything else
stays out of the way.

- **While bank jpot x1 armed:** eth→UART inject is only winner x1 + progressive-issue
  frames (`progwinval>0`). All other eth inject is transaction-deferred. Hub peer
  gameplay yields for the whole arm window.
- **On arm:** cancel competing `_pending` into Queue B; lift armed x1; reorder.
  Events: `bank_jpot_x1_priority_cancel`, arm fields `cancelled_pending`.
- **Master retries preserved:** never eth-inject-dedupe bank progressive frames;
  progressive issue bypasses await_x1 peel / post-x9 grace / inject-abort cool while
  close is armed. Do **not** starve Master 0x49 retries onto the seats.
- **Close path speed:** bank-jpot x1 uses p0-style HS ready (skip post_hs settle).
- **Fanout:** only bank progressive-win x9 full-floods for bells. Cash/other x9 with
  unknown TID→port no longer flood all hops (`only_ports=[]`). Fix:
  empty `only_ports` was treated as flood (falsy list).
- Cash eth inject dedupe window while armed: **2000 ms** (non-jpot only).

Rollback: `3.0.1.26` (superball-sheet11).

## 3.0.1.26 — 2026-08-04 — superball-sheet11

Phase 2 SuperBall cash uses **SGVI sheet 11** mult matrix (not TouchEasy sheet 10).
Validated against bank help paytable at 1 credit / $0.25 (pos5 `T340_0002`).

- TouchEasy (9309) unchanged — sheet 10.
- SuperBall (930A): sheet 11 cash + last-ball **×4** when 20th ball is a pick hit.
- BAJ (Buy-A-Jackpot) tables **not** evaluated; progressive remains Phase 1 via
  `progwinval` (`progwin_phase1` skip). Field BAJ option (e.g. table 10) only
  changes when the game routes progressive vs cash.

Rollback: `3.0.1.25` (superball-keno-p2).

## 3.0.1.25 — 2026-08-04 — superball-keno-p2

Phase 2 cash early-win: **SuperBall Keno (930A)** in the allowlist (was
`game_unsupported`).

- Initially reused sheet-10 mults (corrected in 3.0.1.26 to sheet 11).
- **Last draw ball** (20th ball @ offset 21+19) in the player's picks → cash win **×4**.
- Progressive / Buy-A-Jackpot still **Phase 1** only (`progwin_phase1` skip when
  `progwinval != 0`).
- Events: `early_win_keno_eval` / `_hit` include `last_ball_hit`, `superball_x4`,
  `base_pay_mult`, `base_win_cents`.

Rollback: `3.0.1.24` (bank-jpot-x1-prio).

## 3.0.1.24 — 2026-08-04 — bank-jpot-x1-prio

Inject priority class: after Master bank progressive jackpot (`progwin>0` x9 / skill grant),
the winner seat’s gameplay **x1** eth→UART inject is **head-of-queue** for 30 s — ahead of
lab synth / foreign `80` / STOM FIFO peers.

- **Arm** on Master `uart_to_eth` and on eth_rx of bank jpot (`bank_jpot_x1_priority_arm`).
- **Class** `INJECT_PRIORITY_BANK_JPOT_X1` (20) between STOM (10) and local P0 status-0D synth (30).
- Never transaction-deferred; lifted from Queue B; grace holds bypassed like STOM.
- Events: `bank_jpot_x1_priority_arm` / `_clear` / `_lift`; `eth_rx_queued` adds
  `bank_jpot_x1_priority` + `inject_priority`.

Root-cause fix for star soak where hub `queue_wait_us≈429ms` delayed progressive `0x81`
while Master waited (meter/reset failure). Rollback: `3.0.1.23` (hub-hot-enroll-eth).

## 3.0.1.23 — 2026-08-03 — hub-hot-enroll-eth

Field hub/spoke: Master that is already running must learn new spokes **without restart**.

- **`DirectionalEthRouter.add_port`**: hot-open AF_PACKET on `eg-sNN` after live enroll.
- **Eth RX select**: refresh `router.sockets()` every poll (was cached at thread start — new
  gretaps never appeared in `select`).
- **Enrollment hub**: `on_peer_ready` after accept / restore / reuse → `add_port` +
  `.active_spoke_gretaps` append. Prefer request `spoke_id` / `EDGE_SPOKE_ID` when free.
- **Spoke auto-enroll**: topology `spoke` posts UDP enroll retries to hub Endpoint from
  `wg0.conf` (events `spoke_enroll_response` / `*_error`).
  Bridge runs as topbox: `/etc/wireguard` is often root-only — enroll must tolerate
  `PermissionError` (`EDGE_HUB_IP` env preferred) and never abort the UART session.
- HMAC + open-window / allowlist rules unchanged. `peers.json` remains live roster.

Rollback: `3.0.1.22` (lab-withhold-status-0d).

## 3.0.1.22 — 2026-08-02 — lab-withhold-status-0d

Lab tool for intentional Slave MRT on a seat with POG-CC (pos5): withhold the
local synth `0x0D` after eth→UART `x9` / local `x1` for a limited number of
rounds so the T340+ times out (~125 ms). Correlate with Heltec
`POG-CC SDS error pending` stamps (DF `2026-08-02-g+`).

- **Python:** `--lab-withhold-status-0d` /
  `FIXTURE_LAB_WITHHOLD_STATUS_0D=1` (alias `LAB_WITHHOLD_STATUS_0D=1`) with
  `*_COUNT` (default **1**). Skips uartd `auto_close` arm and Python
  `status_0d_synth` for those rounds; Master eth `0x0D` still blocked by
  existing `status_0d_block_all`. Emits `lab_withhold_status_0d` /
  `lab_withhold_status_0d_exhausted`. Forbidden with `--prod`.
- **Launcher:** `run_bridge_jun11_lab_withhold_0d.sh` (FIXTURE_POS=5).
- uartd/inject tip string only (no C close-path change). Driver still **2.2.2.10**.

Rollback: `3.0.1.21` (x1-ack-wire-len).

## 3.0.1.21 — 2026-08-02 — x1-ack-wire-len

Pos2 SpinBall progressive unpaid on tip `3.0.1.20` (`2026-08-02_1515-UTC`,
TID `38D7`, progwin **$2,824.56** @ 08:49 EDT): `extract_local_x1_frame` used
first-EOP hunt, but **EOP is `0x0D`**, so a **6 B** false SOH..EOP with local
TID was emitted as `IPC_X1_ACK_FRAME`. Python eth-forwarded junk
(`peer_len=6`, `peer_msgs=null`), latched `early_x1_eth_done`, then **skipped**
the real 12 B `0x81` in the post-inject peer path. Master saw `80`→`89` +
bank WLAN jackpot and **never** eth `81` — meter did not reset.

- **uartd:** `extract_local_x1_frame` requires gameplay-ACK **wire length 12**
  (same as `pog422_wire_len_primary` / `COMMAND_WIRE_LEN`); no first-EOP hunt.
- **Python:** validate local wire-len ACK before eth-forward; on reject emit
  `inject_x1_ack_early_eth_reject` and **do not** latch `early_x1_eth_done`.
  Do not merge short X1_ACK into `post_de_rx`.
- Driver still **2.2.2.10**.

Rollback: `3.0.1.20` (x1-ack-eth-before-0d).

## 3.0.1.20 — 2026-08-01 — x1-ack-eth-before-0d

Pos4 SpinBall progressive overnight (`2026-08-01_0318-UTC`, TID `37BC`,
progwin **$3,126.80**): uartd peeled local `0x81` (`harvest=4` imm) and
synth'd `0D` **before** Python could eth-forward the ack to Master. Hub saw
Master `0x89` retries (`outcome_retry`) and **no `0x81` for `37BC`** — Slave
showed Jackpot Won (progwin on `0x89`) while Master likely skipped progressive
clear / pay accounting.

- **uartd:** extract full local x1 SOH..EOP; emit `IPC_X1_ACK_FRAME` **before**
  synth `0D` inject; merge imm peel into luxury `POST_DE_RX` when post_de was
  skipped because x1 was already found.
- **Python:** mid-inject `on_x1_ack_frame` eth-forwards ack immediately (during
  HS-idle wait / 0D TX); skip duplicate post-return early eth; merge x1 into
  `post_de` peer path.
- Driver still **2.2.2.10**.

Rollback: `3.0.1.19` (auto-close-skip-closed-seq).

## 3.0.1.19 — 2026-07-31 — auto-close-skip-closed-seq

Pos5 SuperBall Keno MRT soak (`2026-07-31_1901-UTC_pos5-mrt-1611-edt-hs-timing-noroll`)
on 3.0.1.18: peel/`synth_done` usually worked, but Master same-seq eth→UART `x9`
barges after close still armed C auto-close → **1353/1392** false
`status_0d_auto_close_miss harvest=0` while Python correctly logged
`mrt_risk_arm_skip reason=seq_already_closed`.

- **Python:** do not arm uartd auto-close when `play_sequence` is already in
  `_mrt_risk_close_done_seq`; emit `status_0d_auto_close_skip reason=seq_already_closed`.
- Belt-and-suspenders: re-label a late miss the same way if close_done races.
- uartd/inject unchanged (BOM tip string only). Driver still **2.2.2.10**.

Rollback: `3.0.1.18` (auto-close-0d-wait-hs).

## 3.0.1.18 — 2026-07-31 — auto-close-0d-wait-hs

Pos5 MRT ~09:46 EDT on 3.0.1.17 (`2026-07-31_1344-UTC_pos5-30117-mrt-after-restart`):
immediate peel found local x1 (`harvest=4`) then synth `0D` failed with
**EBUSY (16)** because HS was still high and `require_idle=0` means *reject if
busy*, not force-TX. Python then `uartd_owns_close` → `late_0d_timeout`.

- **uartd auto-close 0D:** `require_idle=1` + **150 ms** HS-idle wait — same
  collision model as other eth→UART traffic; **do not stomp** an active x1 window.
- If 0D still fails after harvest, stash the built frame and **retry when HS
  falls** (`auto_close_0d_idle_retry_*`) within the pending window.
- Python: label `harvest=4` as `immediate_post_x9`.

Rollback: `3.0.1.17` (x9-immediate-rx-peel).

## 3.0.1.17 — 2026-07-31 — x9-immediate-rx-peel

Pos1 MRT near midnight (`2026-07-31_1142-UTC_pos1-mrt-near-midnight-edt`): after a
good local eth→UART x9 echo, uartd still returned `NO_X1` (`harvest=0`,
`post_de_rx_len=0`) then re-injected Master's same-seq x9 retries at ~120 ms —
holding the bus while the Slave's x1 (ms after hearing x9) was missed. Blind /
infer `0D` is not the fix.

- **uartd:** after armed local x9 DE-off, primary HS-agnostic RX peel:
  **20 ms first-activity gate** (any UART bytes), extend while RX continues
  (idle slice), complete on framed local x1, **45 ms hard cap**. `harvest=4`
  (`immediate_post_x9`). Zero activity in 20 ms → existing post_de / HS backup.
- **Python:** defer same-seq eth→UART x9 while `await_x1` with
  `inject_deferred reason=await_x1_peel` (marks `retry_seen` so no-retry infer
  does not fire while Master keeps eth-retrying).
- Driver unchanged (**2.2.2.10**).

Rollback: `3.0.1.16` (hs-sample-tristate).

## 3.0.1.16 — 2026-07-30 — hs-sample-tristate

Fix the `slv_hs_misalign` false-alarm storm introduced by 3.0.1.15. Archive
`2026-07-30_2336-UTC_pos5-hs-de-hold-verify-30115` confirmed the DE fix held
(1293 HS windows, max **26.9 ms**, zero > 100 ms) but logged **1442**
`slv_hs_misalign` — exactly two per inject (`out_on` + `after_drain`), all
`slv_in=false`. 3.0.1.15 replaced the DE-high `slv_in_phase_active()` samples
with `gpio1_tr.last_active`, but nothing polled the trace between
`gpio1_trace_init()` (pre-DE, seeds 0 on an idle line) and the snapshots, so the
field was frozen low and the bridge scored every inject as a misalignment. Same
false-alarm shape 2.2.1.14 fixed after 2.2.1.13, via a different route.

- **inject:** new `gpio1_trace_poll_event()` — `GET_GPIO1_EVENT` only, which
  copies the driver's cached interrupt state and costs **no USB control
  transfer**, so it is safe while DE is high. Polled each drain-loop slice and at
  both snapshot points.
- **Tri-state SLV-IN:** `POG422_SLV_IN_INACTIVE|ACTIVE|UNKNOWN` (0/1/2).
  DE-high samples report UNKNOWN when the GPIO1 event cache is off instead of
  masquerading as low. `slv_in_before` / `slv_in_after_off` stay boolean (both
  are sampled outside DE).
- **Python:** `_slv_in_state()` maps 2 → `None`; bridge emits `slv_hs_misalign`
  only on a definite `False`.
- No change to DE timing, bulk write, or capture. Driver stays **2.2.2.10**.

Watch on the next soak: if the GPIO1 interrupt cache lags our own DE assert,
misalign could still fire on a real-but-stale low. The Pi GPIO HS watcher
(`hs_gpio_machine.jsonl`) is the better oracle and should own this alarm if so.

Rollback: `3.0.1.15` (de-hold-hard-cap).

## 3.0.1.15 — 2026-07-30 — de-hold-hard-cap

Kill the pos5 five-second SLV-OUT / HS stalls. Archive
`2026-07-30_2211-UTC_pos5-hs-game-classes-no-mrt` proved both outliers were
**our** MANUAL_DE: bulk/`tx_ns` finished in ~0.1–0.3 ms with
`de_release_reason=temt`, yet `de_hold_us` stayed ~5.03–5.25 s until
`gpio7_sl_out(0)` returned. Root cause: generic `xr_usb_serial_set_reg` /
`usb_control_msg` uses a **5000 ms** timeout, and the DE-high path still called
`capture_poll` / `GET_REG` (via `inject_poll_trace` and `slv_in_phase_active`)
before release. A wedged control EP therefore held GPIO7 for a full five seconds
— exactly the Python `INJECT_WAIT_S_DEFAULT` / USB ctrl ceiling — while the
driver's between-chunk wall-clock never ran.

- **Driver 2.2.2.10:** `pog422_set_gpio7_sl_out` uses a dedicated **100 ms**
  USB control timeout (not 5000). MANUAL_DE arms a delayed-work hard-cap at
  wire-air+20 ms (15–300 ms clamp); `SET_GPIO7(0)` and error paths disarm it.
- **inject:** no `capture_poll` / GET_REG while DE is high; HS snapshot from the
  GPIO1 trace only; release GPIO7 immediately after TEMT/deadline.
- **Python:** `INJECT_WAIT_S_DEFAULT` 5.0 → **0.75** (IPC hang ceiling, not DE).
- Requires XR module **2.2.2.10** (`EG_COMPONENT_MIN_VERSION`).

Rollback: `3.0.1.14` (clear-capture-keep-hs-gpio).

## 3.0.1.14 — 2026-07-30 — clear-capture-keep-hs-gpio

Bench instrumentation fix. `CLEAR_CAPTURE=1` wiped **all** of `data/` at bridge
startup, including a `hs_gpio_machine.jsonl` already opened by a foreground
`hs_gpio_logger` in another terminal. Unlinking the path left the logger writing to
an orphaned inode: it counted 1276 edges and printed a healthy summary, while `ls`
and rclone saw nothing. Two pos5 HS-timing soaks were lost that way.

- `clear_capture_files()` and `pog422_wipe_capture_dirs()` now preserve an `hs_gpio*`
  companion capture **only while a live process holds that inode open** (dev/inode
  scan of `/proc/*/fd`, not readlink text, since an unlinked fd reads back as
  `… (deleted)`). An unheld leftover is still deleted: keeping it would fold a
  previous run's edges into this session's archive, so the analyzer would correlate
  HS timing against the wrong `events.jsonl` — a silent wrong answer, worse than no
  data. Both outcomes are logged (`preserve (open by a live logger)` /
  `removed stale companion capture`).
- `hs_gpio_logger.EdgeWriter` compares its fd inode against the path on every
  write/flush; if something unlinked or replaced the file it reopens and emits a
  `type=reopen` marker, so edges stay visible to sync.
- `run_hs_gpio_logger.sh` sourced `${ROOT}/source_ethergold_env.sh`, which does not
  exist (env lives in `sync/`) — that is why the banner reported `pos=0`. Fixed, and
  the tap line now comes from `hs_gpio.env` (pos5 J19 = **GPIO3**) with a numeric
  guard, so a typo like `HS_GPIO_LINR=3` can no longer fall through to GPIO9.

Covered by `tests/test_clear_capture_preserve.sh` (both wipe paths, held-open vs
stale, inode identity, and the reopen marker) — run via `tests/run_all_unit.sh`.

No wire, parser, or checksum change — capture-file lifecycle and diagnostics only.
uartd/inject rebuild is required only to keep the BOM string equal to tip.

Rollback: `3.0.1.13` (bank-jpot-grant-flood).

## 3.0.1.13 — 2026-07-30 — bank-jpot-grant-flood

Extend `3.0.1.12` to the skill jackpot round trip. `3.0.1.12` keyed off
`is_gameplay_response()`, which is `{0x29,0x39,0x49,0x59,0x69,0x79,0x89}` — it does
**not** include the grants `0x5A`/`0x7A`, and those are exactly where Draw Poker
royals and SpinJack bonus progressives carry `progwinval`. Those wins were still
unicast to the winner's spoke, so other T340+ boards stayed silent.

- `is_bank_progressive_jackpot_frame()` now accepts skill jpot grants
  (`is_skill_jpot_grant`) as well as outcome x9, so the Master's answer to a slave
  claim floods to every spoke.
- `maybe_publish_bank_wlan()` had the same blind spot — skill grants never produced
  a clear-WLAN jackpot announce, so DyzineFrame fireworks also missed poker and
  SpinJack progressives on the star underlay. Grants now announce like an x9.
- The slave claim itself (`0x52`/`0x72`) is deliberately not flooded: it carries no
  `progwinval` and is addressed to the Master.

Still not covered: SpinBall Bonus `970D`. It is an 8-liner slot with no documented
claim/grant pair (distinct from SpinJack `960F`, which is blackjack-based and does
use `0x72`/`0x7A`), and `0x89` carries no `progwinval`.

Note: the older bench `0x01` @ 17 B “poker skill reply” (v16aj) was a cropped
packet aberration, not a real Master grant — do not treat it as a flood candidate.

Rollback: `3.0.1.12` (bank-jpot-x9-flood).

## 3.0.1.12 — 2026-07-30 — bank-jpot-x9-flood

Restore bank-wide T340+ bell on the wlan-star underlay. Since `2.2.2.71`,
`EDGE_TID_UNICAST` pinned every gameplay x9 to the winner's spoke, so a
progressive jackpot on one seat never reached the other T340+ boards and only the
winner's cabinet could ring. On copper every slave saw that frame natively.

- `is_bank_progressive_jackpot_frame()` in `pog422_core.py` — gameplay x9 whose
  `progwinval` (BE32 @ 13–16) is non-zero. Wager/ack/`0x0A` and zero-progwin
  outcomes are unaffected.
- `_eth_only_ports_for_frame()` returns flood for those frames, so hub UART→ETH
  and ETH→ETH relay both reach every spoke. Ordinary gameplay stays unicast, so
  fan-out cost is unchanged except on an actual jackpot.
- Defer queue no longer swallows a bell: `_abandon_transaction_deferred()`
  promotes bank jackpot frames to `_pending` (`source=bank_jpot_late`) instead of
  dropping them, and `queue_cap` eviction skips them. A late bell is acceptable;
  a missing one is not.
- `transaction_defer_abandon` gains `kept_bank_jpot`.

Slot progressives that carry no `progwinval` on `0x89` (SpinBall `970D`) are still
not covered — that needs the symbol-map work in `SLOT_8LINER_EARLY_WIN.md`.

Rollback: `3.0.1.11` (ohd-fireworks-http).

## 3.0.1.11 — 2026-07-29 — ohd-fireworks-http

Expose the existing atomic `fireworks-request.json` event to Win11 OHD signs.

- `ethergold-http` serves `GET /EtherGold/fireworks-request.json` from the same
  `ETHERGOLD_ROOT` used by the bridge and returns `{}` while no request exists
  (quiet normal polling, no repeated 404 access logs).
- The endpoint is unauthenticated like `progressives.csv`; it contains only the
  existing display event payload and is polled by OHD on the trusted bank LAN.
- No progressive detection, wire timing, UART, or inject behavior changed.

Rollback: `3.0.1.10` (hs-gpio-ground-truth).

## 3.0.1.10 — 2026-07-29 — hs-gpio-ground-truth

Bench instrument: Pi GPIO tap on `SLV_HS` so HS edge timing has a reference the
USB paths cannot provide. Evidence-only — no wire-path or bridge behavior change.

- `hs_gpio_logger.py` — drives the GPIO v2 character-device uAPI directly via
  `ioctl` (no libgpiod binding: Bookworm ships v1, Trixie v2). Kernel-stamped
  edges on `CLOCK_MONOTONIC`, the same base as uartd and the XR driver, so
  cross-source deltas are valid. Records `recv_ns` separately so delivery lag is
  measured rather than assumed, and emits `seq_gap` on event-buffer overflow.
  Matches the controller by **label** (`pinctrl-rp1` / `pinctrl-bcm2711`) because
  Pi 5 / CM5 renumbered the RP1 gpiochip between kernel releases.
- `hs_gpio_analyze.py` — true HS width census (sub-3 ms buckets), real GET_REG
  poll lag, and how many HS-active windows each USB path misses outright.
  Excludes widths spanning a dropped-event gap.
- `run_hs_gpio_logger.sh` — foreground launcher; writes
  `${OUTPUT_DIR}/hs_gpio_machine.jsonl` beside `events.jsonl` so
  `archive_session.ps1` picks it up. Touches no `/dev/ttyXRUSB*` and no uartd
  socket, so it runs concurrently with the bridge.
- `HS_GPIO_TAP.md` — wiring (tap `SLV_HS` at R6/LED6, **not** `SLV-IN` — the
  TXS0108E one-shot stage is load-sensitive), J19 pin 7 = GPIO9 on the Seeed CM4
  Dual GbE carrier, and the CM5/RP1 constraint that rules out pigpio.
  **`SLV_HS` is a 5 V signal** (HAT `VCC` = +5 V; only `VDD` is 3.3 V — the reason
  the TXS0108E exists). Pi GPIO is 3.3 V only and RP1 is not 5 V tolerant, so the
  tap requires a 74LVC1G17 buffer or a 1.8k/3.3k divider; a plain series resistor
  is not protection. Corrected before any hardware was built.

Motivation, from `sessions/2026-07-29_1937-UTC_pos5-hs-timing-kernel-ring`: the
XR Table-8 interrupt endpoint is hard-quantized to **8 ms** (every inter-arrival
an exact multiple; HS widths are integer slot counts), and the GET_REG poll
floors at ~2.75 ms with observed HS minimum 3.02 ms — the short-window
population piles up at the instrument's own resolution limit.

Both new scripts carry `--self-test` (uAPI struct/ioctl encodings; synthetic
edge/width/miss logic) so they can be validated on the PC before deploying.

Rollback: `3.0.1.9` (exar-only-fuser).

## 3.0.1.9 — 2026-07-29 — exar-only-fuser

Stop EtherGold driver reload from killing DyzineFrame / PermDevMapper on the P3 FTDI cable.

- `_stop_fixture_uart_users` in `sync/update.sh` only `fuser -k`s Exar nodes
  (`/dev/ttyXRUSB*` and `ttyUSB*` with udev `ID_VENDOR_ID=04e2`).
- FTDI P3 (`0403:6001`), `/dev/ser_DT1`, and `/dev/pogcc` are never swept.

Rollback: `3.0.1.8` (hs-timing-driver-floor).

## 3.0.1.8 — 2026-07-29 — hs-timing-driver-floor

Make the timing-ring driver install automatic during the normal update.

- Raise the XR driver floor from `2.2.2.8` to `2.2.2.9`; `update.sh` now sees
  2.2.2.8 as out of range and installs/reloads 2.2.2.9 without `FORCE_DRIVER=1`.
- Runtime timing remains opt-in through `POG422_TIMING_TRACE=1`; the 2.2.2.9
  ring is dormant otherwise and does not change the wire path.
- Deploy staging now validates the staged component floor against source instead
  of freezing a historical driver version in the build script.

Rollback: `3.0.1.7` (hs-timing-env).

## 3.0.1.7 — 2026-07-29 — hs-timing-env

Per-seat env-file switch for the 3.0.1.6 timing trace (pos5 only for now).

- `sync/enable_hs_timing_env.sh` upserts `POG422_TIMING_TRACE` into
  `~/EtherGold/fixture-bridge.env`; refuses seats other than 5 / 51 without `--force`.
- `source_ethergold_env.sh` exports `POG422_TIMING_TRACE*` (allowlist — otherwise uartd
  never saw the key from the env file).
- uartd default trace path is now `${OUTPUT_DIR}/hs_timing_<channel>.jsonl` so it lands
  beside `events.jsonl` and `CHANNEL=both` daemons do not share one file.

Rollback: `3.0.1.6` (hs-timing-trace).

## 3.0.1.6 — 2026-07-29 — hs-timing-trace

Runtime-gated HS / RX / auto-close timing diagnostics (evidence only).

- Driver `2.2.2.9`: always-compiled timing ring; `SET/GET/RESET/DRAIN_TIMING_TRACE` (off by default).
- uartd: `POG422_TIMING_TRACE=1` → buffered JSONL (`data/hs_timing.jsonl`); no burst-on-fall change.
- `hs_timing_analyze.py` + `HS_TIMING_DIAG.md` + fixture self-test.
- Independent of `POG422_HW_DEBUG` (that mode still changes RX drain).

Rollback: `3.0.1.5` (c-close-always).

## 3.0.1.5 — 2026-07-28 — c-close-always

C owns local status `0D` after x1 — always (no Python uart_window fallback).

- After inject `NO_X1`, keep pending x9 ≤200 ms; HS WINDOW/grace still hard-RT closes.
- Bridge skips Python synth queue when `uartd_inject`; late `AUTO_CLOSE_DONE` bookkeeping.

Rollback: `3.0.1.4` (auto-close-hs-x1).

## 3.0.1.4 — 2026-07-28 — auto-close-hs-x1

Pos5 Shamrock 7's MRT 14:56 EDT: copper `51` completed as `uart_window` while C
auto-close returned `NO_X1` (echo=self-`59`, `post_de_len=0`); Python late `0D`.

- Stash local-TID x1 from HS WINDOW flush while auto-close armed.
- If echo+post_de miss, wait/drain HS burst or FIFO (≤40 ms) and peel (`harvest=3`).
- C still injects synth `0D` before DONE on that path.

Rollback: `3.0.1.3` (uartd-auto-reinit).

## 3.0.1.3 — 2026-07-28 — uartd-auto-reinit

Field CGS after pos5 XRUSB/HS thrash: uartd died (`bad ipc magic 0x0` then
zombie) while bridge kept running → forever `uartd not connected`.

- Bridge **auto-respawns** pog422_uartd on IPC death / zombie (wait for tty,
  respawn binary, reconnect socket). Events: `uartd_auto_reinit`.
- `uartd_common.sh` exports `POG422_UARTD_{PID,PORT,CHANNEL,LOG,BIN}` for adopt.

Rollback: `3.0.1.2` (abort-de-hs-peel).

## 3.0.1.2 — 2026-07-27 — abort-de-hs-peel

Pos5 17:37 MRT follow-up on uart-auto-close:

- **IPC write deadline (100 ms):** never pin `ipc_mu` forever on a full client
  socket (DONE write blocked → GPIO7 abort ACK never sent → `aborted_de=0`).
- **GPIO7 RELEASE retries** (3×) + best-effort ACK after DE drop.
- **Auto-close HS peel:** keep incremental RX while SLV-HS already high after
  local x9 (was `burst_on_fall` → FIFO held until fall → `NO_X1`); arm fall
  capture after peel.
- **Python:** abort RELEASE retries; longer orphan drain; `inject_abort_cooldown`
  before eth→UART requeue barge while HS still up.

Rollback: `3.0.1.1` (uart-auto-close).

## 3.0.1.1 — 2026-07-27 — uart-auto-close

Minor-feature BOM spin for under-the-hood hard-RT architecture: local close moves
into uartd. After eth→UART x9, harvest peer x1 and inject synth `0D` in C before
any Python IPC. `INJECT_DONE` precedes echo/forensics so `_tx_lock` cannot pin on
Dropbox/logging (Class J / pos5 15:10 MRT: `inject_total_us≈19ms` vs
`inject_ioctl_us≈886ms`). Python arms trailer + logs
`status_0d_synth_done source=uartd_auto` after the fact.

- BOM stamp tip → **3.0.1.1** (rebuild uartd/inject on Pi).
- Same change briefly stamped `3.0.0.13` before this renumber.

Rollback: `3.0.0.12` (current-game-catalog).

## 3.0.0.12 — 2026-07-27 — current-game-catalog

- Expand `WIRE_GAME_TO_INDEX` from lab seed (7) to **13/16** titles from the
  2026-06-19 wire-len census (`9400/01/03/04`, `9206`, `9107`, `9508`/`9507`,
  `9309`/`930A`, `970C`/`970D`, `960E`/`960F`). Still unmapped until capture:
  Wild Jokers, 8-Ball Poker, Lightning Keno.
- BOM stamp tip → **3.0.0.12**.

Rollback: `3.0.0.11` (current-game-ipc).

## 3.0.0.11 — 2026-07-27 — current-game-ipc

- **Current-game IPC for DyzineFrame:** bridge maps local-TID M/S gameplay
  `game_id_hex` → DF `curr_index` (1..16) and publishes `~/EtherGold/current-game.json`
  only when the index changes (fireworks-style atomic JSON).
- Env `ETHERGOLD_CURRENT_GAME_IPC`: `0` off (default), `readonly` compare/log only,
  `1` write-on-change. Events: `current_game_would_publish` / `current_game_published`.
- Incomplete wire map (Touch6/Bingo/Keno/SuperPick/SpinJack/Triple7s/SpinBall); unmapped skip.
- BOM stamp tip → **3.0.0.11**.

Rollback: `3.0.0.10` (field-ui-drop-spe).

## 3.0.0.10 — 2026-07-27 — field-ui-drop-spe

- Remove SPE from field config UI / profiles (LAN8670 multidrop is not galvanically
  isolated — does not solve mixed 3-phase common-mode). Stale `profile=spe` /
  `EDGE_UPLINK=spe` still rejected.
- BOM stamp tip → **3.0.0.10**.

Rollback: `3.0.0.9` (inject-abort-de-drop).

## 3.0.0.9 — 2026-07-27 — inject-abort-de-drop

- **Kill pos5 02:51 MRT class:** orphan `INJECT_TX` after gameplay timeout left
  DE/HS high ~5 s; Python returned `None` with no abort; `GET_SLV_IN` blocked ~2 s.
- **uartd:** inject on worker thread; main services `GPIO7_RELEASE` / `GET_SLV_IN`;
  concurrent inject → `EBUSY` DONE; early `IPC_INJECT_ECHO` when framed echo ready
  (arms Python `done_extend` before TEMT/DONE).
- **Python:** timeout → `gpio7_release` + orphan IPC drain; gameplay requeue
  (`False`); `GET_SLV_IN` ≤50 ms; side-queue inject IPC; pre-inject HS from cache.
- **Driver 2.2.2.8:** MANUAL_DE wall-clock — drop GPIO7 if bulk exceeds
  `timeout_ms+100ms` (floor `EG_COMPONENT_MIN_VERSION=2.2.2.8`).
- BOM stamp tip → **3.0.0.9** (rebuild uartd/inject **and** XR module).

Rollback: `3.0.0.8` (inject-log-after-tx).

## 3.0.0.8 — 2026-07-26 — inject-log-after-tx

- **Inject critical path:** do not flush `inject_echo.jsonl` / `packets.raw`
  under `_tx_lock`. Queue forensics in memory; run peer harvest, synth `0D`,
  and pending injects first; write logs after close path.
- Fixes pos4 20:55 EDT MRT class: ~1.2s Dropbox `flush` after a ~28ms UART
  inject delayed synth close while SLV markers looked like an orphan hold.
- BOM stamp tip → **3.0.0.8** (rebuild uartd/inject on Pi for version match).

Rollback: `3.0.0.7` (uart-xrusb-reopen).

## 3.0.0.7 — 2026-07-26 — uart-xrusb-reopen

- **UART XRUSB reopen:** when Exar USB drops/renumbers (`ttyXRUSB1 (deleted)`
  while `0`+`2` remain), `pog422_uartd` detects dead fd, emits
  `uart_port_lost` / `uart_port_reopen`, re-resolves by channel (ois=lowest,
  machine=prefer `1` else highest), and reopens without killing Python/WG.
- **`--port`** is first-open hint only; attach/reattach always use channel
  resolve. Launchers pass `--channel`.
- **Queue hold:** eth→UART `_pending` defers while port down; unsuccessful
  transfers (ENODEV/EIO/timeout during loss) requeue — not terminal
  `inject_failed`. Events: `inject_requeued_uart_lost`.
- BOM stamp tip → **3.0.0.7** (rebuild uartd/inject on Pi).

Rollback: `3.0.0.6` (pipe-probe).

## 3.0.0.6 — 2026-07-26 — pipe-probe

- **EDGE pipe probe:** bidirectional PING/PONG over WG+gretap (`EGPIPE1`, eth
  tag `P`) — **never** UART-injected to POG422. Retries until first success;
  records `pipe_ready_delay_s` / attempts / RTT; optional ongoing RTT samples.
  Events: `pipe_probe_start|tx|rx|retry|ready|rtt|miss`. Default on when
  `EDGE_MODE=1` (`PIPE_PROBE=0` to disable).
- Addresses first-game aborts where spoke `eth_tx` ran but hub saw no uplink
  for ~15–20 s (see session eval 2026-07-26_1529).
- BOM stamp tip → **3.0.0.6**.

Rollback: `3.0.0.5` (field-config-ui).

## 3.0.0.5 — 2026-07-26 — field-config-ui

- **Field configuration UI** on ethergold-http (`/EtherGold/config`): login
  (default SGVI), profile radios, save/validate/apply, `field-config.json` →
  `/etc/ethergold/{role,ethergold,edge}.env`. See `FIELD_CONFIG_MATRIX.md`.
- **Identity:** free-text `asset_id` + bank; spoke unique ID = POG422 **TID**;
  no field `FIXTURE_POS` (derived internal).
- **Always CHANNEL=both**; dual-channel **listen** (machine + OIS status hooks;
  OIS not forwarded to OHD progressives HTTP).
- **OIS off VPN:** `ois_wifi_plane` HMAC flood on clear 6 GHz WiFi when
  `OIS_WIFI_PLANE=1`; inject with no RX TID filter. Machine stays on WG+gretap.
- **Atlas star + DyzineFrame:** `set_ethergold_role.sh edge --display-on`
  separates transport from display.
- BOM stamp tip → **3.0.0.5**.

Rollback: `3.0.0.4` (master-uart-x9-no-synth).

## 3.0.0.4 — 2026-07-25 — master-uart-x9-no-synth

- **Master vs Slave synth 0D:** track per-round x9 origin (`uart` vs `eth`).
  UART-origin local x9 (Master native gameplay response) ⇒ **never** synth 0D —
  POG422 closes with native UART `0x0D`. Eth-injected local x9 (Slave) still
  arms synth after local x1. Skip reason: `master_uart_x9_native_0d`.
- BOM stamp tip → **3.0.0.4**.

Rollback: `3.0.0.3` (hub-peer-inject).

## 3.0.0.3 — 2026-07-25 — hub-peer-inject

- **Hub artificial freeze:** with `EDGE_TOPOLOGY=hub`, one local close
  (`await_local_0d` / up to **2 s** `await_local_x1`) was blocking **all** other
  TIDs' eth→UART gameplay — every other spoke waited in `transaction_deferred`.
  Soft **0x0A** still defers (latest-wins). Other spokes' gameplay now proceeds;
  same-TID close path + P0 synth 0D unchanged. Copper single-Master (no hub
  topology) keeps the old serialize-on-close behavior.
- Soak evidence: spoke req→reply p50≈125–150 ms, p90≈1 s+, unmatched CGS;
  hub `transaction_defer await_local_0d` and inject-hold were the pipe stalls —
  not gretap fan-out (p90≈0.12 ms).
- BOM stamp tip → **3.0.0.3**.

Rollback: `3.0.0.2` (spoke-tunnel-fix).

## 3.0.0.2 — 2026-07-25 — spoke-tunnel-fix

- **Spoke launch crash:** `tunnel_up` mis-read gretap endpoints (`ip -d` prints
  `remote…local…`; checker wanted `local…remote…`), treated a good `eg-sNN` as
  mismatch, then fell through to legacy `gretapN` and died on
  `gretap0@NONE` / `File exists`. Spokes now use `ensure_spoke_gretap` only.
- **pos4 leftover `bench.env`:** `POG_EDGE_ROLE=pos4` overrode `EDGE_ROLE=spoke`.
  Canonical `EDGE_TOPOLOGY=hub|spoke` from `/etc/ethergold/edge.env` now wins;
  provision retires single-hop `bench.env`.
- **Stale live WG:** when `wg0` already up, hub/spoke syncs from `wg0.conf`
  (`wg syncconf` / bounce) instead of only patching Endpoint on the old peer.
- BOM stamp tip → **3.0.0.2**.

Rollback: `3.0.0.1` (hub-spoke).

## 3.0.0.1 — 2026-07-25 — hub-spoke

- **Major BOM cut:** wlan-star hub/spoke underlay is the product line for all-wireless
  banks (no inter-seat eth). Prior `2.2.2.71`–`2.2.2.75` work is the development path;
  field/tip BOM is now **`3.0.0.1`**.
- **Topology:** `EDGE_MODE=1` + `EDGE_TOPOLOGY=hub|spoke`; Master = hub, Slaves =
  spokes with direct WG tunnels; TID-unicast gameplay + clear-WLAN meters/jackpots.
- **One edge.env:** `/etc/ethergold/edge.env` canonical; `/etc/fixture/edge.env` is a
  symlink on provisioned seats. One command per seat:
  `cd ~/EtherGold/dev && bash run_ethergold.sh`.
- **Provision:** `provision_hub_spoke.sh` + `sync/provision_wlan_star.ps1` pre-seed
  WG identity / `peers.json` so a zero-peer hub can bind `eg-sNN` on first start.
- XR driver still OK in **`[2.2.2.7, 3.0.0.1]`** (range check). uartd/inject must
  equal tip.
- BOM stamp tip → **3.0.0.1**.

Rollback: `2.2.2.75` (edge-env-canonical) for emergency tip only.

## 2.2.2.75 — 2026-07-25 — edge-env-canonical

- **Single edge.env — no fork:** `/etc/ethergold/edge.env` is the one canonical
  config. `/etc/fixture/edge.env` becomes a **symlink** to it on provisioned
  seats (`.74`'s dual-write mirror retired — two real files can diverge).
  `run_ethergold.sh` / `pog_edge_env.sh` back to single-source: canonical if
  present, else legacy plain-file fallback for unmigrated boxes.
- **provision_wlan_star.ps1** writes only the canonical file, parks any real
  legacy file as `edge.env.retired.<stamp>`, and installs the symlink.
- **apply_chain_machine_uartd.sh** CRLF strip uses `sed --follow-symlinks`
  (plain `sed -i` would replace the legacy symlink with a diverging copy).
- BOM stamp tip → **2.2.2.75**.

Rollback: `2.2.2.74` (edge-env-overlay).

## 2.2.2.74 — 2026-07-25 — edge-env-overlay

- **One-command star:** `run_ethergold.sh` / `pog_edge_env.sh` source
  `/etc/ethergold/edge.env` **then** `/etc/fixture/edge.env` (fixture wins).
  Stale `EDGE_MODE=0` in the ethergold path no longer silently drops hub/spoke
  config written to fixture. Banner shows `topo=` / `uplink=`.
- **provision_wlan_star.ps1** writes **both** edge.env paths so the next
  provision lands where `run_ethergold` looks first.
- Operator path remains: `cd ~/EtherGold/dev && bash run_ethergold.sh`
  (hub first). No `FIXTURE_POS=` / `run_bridge_edge_bench.sh` prefix needed —
  `fixture-bridge.env` + edge.env drive it.
- BOM stamp tip → **2.2.2.74**.

Rollback: `2.2.2.73` (hub-spoke-provision).

## 2.2.2.73 — 2026-07-25 — hub-spoke-provision

- **Bootstrap fix (provisioning, not parser):** a hub with zero enrolled spokes had no
  gretap to bind — `tunnel_up.sh` fell back to a placeholder `eg-s02` that does not
  exist, and the enrollment listener only starts *inside* the bridge. New
  `pog-edge/provision_hub_spoke.sh` seeds hub `[Interface]` conf, per-spoke
  `wg0.conf` (hub pub + endpoint), and `peers.json` / `allowlist.txt`, so
  `restore_hub_spoke_gretaps` has real `eg-sNN` legs on first start.
- **PC orchestrator:** `sync/provision_wlan_star.ps1` patches `edge.env` per seat
  (`EDGE_TOPOLOGY`, `EDGE_SPOKE_ID`, `EDGE_GAME_IF=none`), collects keys, seeds the
  hub. Config + keys only — operator still starts bridges in the foreground.
- Spoke IDs default to `10 + seat` so the overlay last octet reads as the seat
  (pos1 → `10.99.0.11` / `eg-s11`).
- Live enrollment path unchanged: it now covers re-enroll and virgin seats rather
  than first-star bootstrap.
- BOM stamp tip → **2.2.2.73**.

Rollback: `2.2.2.72` (enroll-crypto-stealth).

## 2.2.2.72 — 2026-07-25 — enroll-crypto-stealth

- **Enrollment replies:** bad HMAC / garbage stay **silent** (no "service exists"
  clue for probes). After a valid fleet HMAC, the hub always replies — accept
  **or** signed diagnostic deny (`bank_mismatch`, `window_closed`,
  `not_allowlisted`, `missing_pub`, `apply_failed`) so commissioning seats know
  what to fix.
- BOM stamp tip → **2.2.2.72**.

Rollback: `2.2.2.71` (hub-spoke-underlay).

## 2.2.2.71 — 2026-07-25 — hub-spoke-underlay

- **Underlay layering:** `EDGE_TOPOLOGY=hub|spoke` extends `EDGE_MODE=1` without
  breaking `EDGE_MODE=0` eth↔eth or single-hop eth↔wlan (`peer-a`/`peer-b`).
  SPE (`EDGE_UPLINK=spe`) reserved until >8-node SPE confirmed. See `pog-edge/EDGE_HUB.md`.
- **TID-unicast + fan-out metrics:** `DirectionalEthRouter` learns TID→port;
  gameplay unicasts when known; `eth_fanout` logs `wall_us` / mode for N=2/4 study.
- **Clear-WLAN bank path:** signed `0x0A` meters + `progwinval>0` jackpot announces
  on non-VPN uplink (`pog_edge_bank_wlan.py`); discovery piggybacks on meter envelope.
- **Identity:** read `MACHINE_ROLE`/`BANK_NUMBER`/`TERM_NUMBER` from DyzineFrame
  `config.ini` with process latch (`pog_edge_identity.py`) — no second `/dev/ttyUSB0`.
- **Enrollment:** spoke keygen + hub UDP listener; live `wg set` + deterministic
  `eg-sNN` gretaps; persist peers; thumb-drive fallback (`pog_edge_enrollment.py`).
- Multi-gretap: stop deleting mismatched sibling gretaps; hub `ETH_PORTS` from live
  spoke set; `FIXTURE_POS` gate `1..99` on EDGE bench launcher.
- BOM stamp tip → **2.2.2.71**.

Rollback: `2.2.2.70` (echo-armed-wait).

## 2.2.2.70 — 2026-07-25 — echo-armed-wait

- **Echo-armed inject wait:** the gameplay 0.55 s budget still fails fast when no
  bus evidence arrives (18:33 anti-pin), but once echo / post_DE / forensics prove
  the TX left the driver, `inject()` extends **once** (`INJECT_DONE_EXTEND_S`
  = 1.0 s) for the late `IPC_INJECT_DONE`. Fixes pos5 2026-07-25 00:09 MRT:
  `79` delivered (`echo_len=42`), DONE ~84 ms past budget → false
  `uartd_inject_timeout` → Master retry → infer close.
- **Socket honors the budget:** per-read recv timeout tracks remaining budget
  (idle bus no longer rides the fixed 2 s recv timeout past 0.55 s and surfaces
  as a bare `timed out`). Timeout detail now reports elapsed, budget, and
  `done_extend` state.
- BOM stamp tip → **2.2.2.70**.

Rollback: `2.2.2.69` (peel-inject-budget).

## 2.2.2.69 — 2026-07-24 — peel-inject-budget

- **Bidirectional peel:** `peer_uart_bytes_after_inject` keeps peer bytes *before*
  and *after* self-echo; `inject_echo_bytes_match` adds `embedded` (pos5 18:33
  leading `61|x9` was discarded → infer).
- **Inject budget:** gameplay eth→UART / synth `0D` IPC wait **0.55 s**
  (`INJECT_WAIT_S_GAMEPLAY`); timeout fails without arming await/infer (stops
  multi-second `_tx_lock` pin). Slow-inject diag includes drain/TEMT proxies.
- **Close policy:** `late_0d` / `already_sent` keep pending when P0 `0D` is
  queued or mid-flight; `_round_close_phase` log label.
- BOM stamp tip → **2.2.2.69**.

Rollback: `2.2.2.68` (x1-always-0d).

## 2.2.2.68 — 2026-07-23 — x1-always-0d

- **Always synth `0D` after a real local `x1`:** remove `status_0d_synth_skip`
  `no_open_await`. Infer/premature `0D` is fine; skipping close when copper/`uart_window`
  `61` arrives after seq already closed was the 18:07 EDT MRT hole.
  `_mark_local_x1_pending` still releases `synth_sent` so a second close can TX.
- BOM stamp tip → **2.2.2.68**.

Rollback: `2.2.2.67` (infer-0d-25ms).

## 2.2.2.67 — 2026-07-23 — infer-0d-25ms

- **Harvest-miss `0D` before slave MRT:** `MRT_X1_INFER_NO_X9_RETRY_MS` and
  `MRT_X1_INFER_AFTER_X9_RETRY_MS` **125/100 → 25 ms**. Slave arms its `0D`
  watchdog when it sends `61` (after we drop DE); waiting 125 ms to infer+synth
  was itself an MRT (pos5 16:57 EDT / w51). Real harvested x1 still closes
  immediately via `.66` early queue.
- BOM stamp tip → **2.2.2.67**.

Rollback: `2.2.2.66` (early-0d-queue).

## 2.2.2.66 — 2026-07-23 — early-0d-queue

- **0D must arm on x1, not on inject DONE:** while eth→UART x9 waits for
  `INJECT_DONE`, `INJECT_ECHO` / `POST_DE_RX` callbacks harvest local x1 and
  `appendleft` synth `0D` onto `_pending` (even under `_tx_lock`).
- `_schedule_synth_attempt` always builds/dispatches (dispatch queues under lock);
  no longer only parks an rk until lock release.
- uartd: skip post-DE poll for non-x9 (stops 25+15 ms stall on every `0D`);
  skip post-DE when DE-high echo already has a peer EOP after self.
- Synth inject: `idle_wait_ms=0` (HS busy → requeue); skip post-HS settle /
  OIS post-x9 grace for P0 close; process pending 0D at end of `send_frame`.
- BOM stamp tip → **2.2.2.66**.

Rollback: `2.2.2.65` (edge-bench-entry-guard).

## 2.2.2.63 — 2026-07-22 — deploy-dev-sync-only

- `To Deploy/EtherGold` is only `dev/` + `sync/` + `install_field.sh`.
  Dropped legacy `bridge/` and `Daemon/` duplicates.
- Field `~/EtherGold/prod` is built on the Pi via `build_prod.sh`.
- BOM stamp tip → **2.2.2.63**.

Rollback: `2.2.2.62` (no-windows-in-deploy).

## 2.2.2.62 — 2026-07-22 — no-windows-in-deploy

- `To Deploy/EtherGold` excludes Windows-only `*.ps1` / `*.bat` / `*.cmd`
  (PC sync helpers stay in workshop `EtherGold/sync` only).
- rclone filter also blocks those extensions on Pi pull.
- BOM stamp tip → **2.2.2.62**.

Rollback: `2.2.2.61` (pull-from-to-deploy).

## 2.2.2.61 — 2026-07-22 — pull-from-to-deploy

- Bench pull root is Dropbox `To Deploy/EtherGold` (`dev/` + `sync/`).
- `build_to_deploy.ps1` stages those folders; `update.sh` pull migrates
  workshop `EtherGold` → To Deploy in `rclone.env`.
- BOM stamp tip → **2.2.2.61**.

Rollback: `2.2.2.60` (update-run-ethergold).

## 2.2.2.60 — 2026-07-22 — update-run-ethergold

- `update.sh` prints one start line: `bash run_ethergold.sh` (role/EDGE from
  env). Optional `UPDATE_START=1` execs it after rebuild.
- BOM stamp tip → **2.2.2.60**.

Rollback: `2.2.2.59` (cli-from-env).

## 2.2.2.59 — 2026-07-22 — cli-from-env

- Operator start is only `bash run_ethergold.sh` — role, EDGE, POS, CHANNEL,
  and prod/bench come from env files; EDGE auto-sudo; listen dispatched from
  `ETHERGOLD_ROLE=listen`.
- `source_ethergold_env.sh` exports `ETHERGOLD_ROLE` / `ETHERGOLD_PROFILE`.
- BOM stamp tip → **2.2.2.59**.

Rollback: `2.2.2.58` (env-pos-autoload).

## 2.2.2.58 — 2026-07-22 — env-pos-autoload

- Launchers load `FIXTURE_POS` / `CHANNEL` from `fixture-bridge.env` (and
  `/etc/ethergold`) — no need to prefix `FIXTURE_POS=N` on the command line.
- Touches: `run_ethergold.sh`, `run_bridge_prod.sh`, `run_listen_prod.sh`.
- BOM stamp tip → **2.2.2.58**.

Rollback: `2.2.2.57` (unified-bridge-entry).

## 2.2.2.57 — 2026-07-22 — unified-bridge-entry

- Single operator entry: `run_ethergold.sh` dispatches copper vs EDGE from
  `EDGE_MODE` + `ETHERGOLD_PROFILE` (prod|bench).
- `run_bridge_prod.sh` / `run_bridge_jun11_good.sh` redirect into
  `run_ethergold.sh` when `EDGE_MODE=1` (unless `ETHERGOLD_ENTRY` already set),
  so copper launchers cannot drop gretap on EDGE seats.
- `ethergold_supervisor.py` starts `run_ethergold.sh` instead of branching
  scripts by hand.
- BOM stamp tip → **2.2.2.57**.

Rollback: `2.2.2.56` (shim-private-reexport).

## 2.2.2.56 — 2026-07-20 — shim-private-reexport

- `pog422_fixture_core` shim re-exports private `_` names (`import *` skips them).
  Fixes `--echo-events` flood of bare `{"ev":"error"}` from
  `cannot import name '_clear_post_x9_grace_state'`.
- `pog422_uart_capture` imports from `pog422_core` directly.
- Echo brief includes `where` / `message` / `errno` so errors are diagnosable.
- BOM stamp tip → **2.2.2.56**.

Rollback: `2.2.2.55` (prod-role-ethergold-rename).

## 2.2.2.55 — 2026-07-20 — prod-role-ethergold-rename

- Field roles: `ETHERGOLD_ROLE=topbox|listen|edge` via `set_ethergold_role.sh` /
  `commission_prod_boot.sh` (default **topbox**).
- Paths: `/etc/ethergold`, `ethergold.service` / `ethergold-collector.service`,
  `/var/lib/ethergold` (legacy `/etc/fixture` + `fixture-prod` migrated once).
- Module rename (compat shims kept): `pog422_bridge` / `pog422_core` / `pog422_listen`,
  `hat_bridge` / `hat_listen`, `ethergold_*_supervisor`.
- BOM stamp tip → **2.2.2.55**.

Rollback: `2.2.2.54` (superpick-9508-replicate).

## 2.2.2.54 — 2026-07-20 — superpick-9508-replicate

- SuperPick synth: bus id **9508** (floor); capture-replicate wager (TID+game+seq+CRC);
  stop random pick rewrite that yielded Master-silent `0x60`.
- BOM stamp tip → **2.2.2.54**.

Rollback: `2.2.2.53` (lab-seat-close-on-x1).

## 2.2.2.53 — 2026-07-20 — lab-seat-close-on-x1

- Lab seat SM: round closes when forged **x1** is sent (no skill/jpot, no 0D synth).
  Inbound Master 0D still accepted by normal bridge path.
- BOM stamp tip → **2.2.2.53**.

Rollback: `2.2.2.52` (lab-seat-sm-eth-rx).

## 2.2.2.52 — 2026-07-20 — lab-seat-sm-eth-rx

- Lab seat SM: also arm x1 ack on **eth_rx** of Master x9 (not only after
  eth→UART inject) — mid-chain synth hops may not complete copper inject.
- BOM stamp tip → **2.2.2.52**.

Rollback: `2.2.2.51` (lab-gameplay-seat-sm).

## 2.2.2.51 — 2026-07-20 — lab-gameplay-seat-sm

- Lab gameplay synth is a **full seat state machine** for all catalog games:
  forge wager (x0) → on Master outcome (x9) forge ack (x1) → bridge 0D close.
- Applies to touch6 / bingo / keno / poker / superpick / spinjack / triple7s
  (`0x2x`…`0x8x` families).
- Dev Lite: `lab_gameplay_synth_ack`.
- BOM stamp tip → **2.2.2.51**.

Rollback: `2.2.2.50` (lab-synth-env-preserve).

## 2.2.2.50 — 2026-07-20 — lab-synth-env-preserve

- `run_bridge_jun11_good.sh`: preserve CLI `LAB_GAMEPLAY_*` across
  `source_fixture_env` (fixture-bridge.env was forcing `LAB_GAMEPLAY_SYNTH=0`).
- BOM stamp tip → **2.2.2.50**.

Rollback: `2.2.2.49` (touch6-autoplay-golden).

## 2.2.2.49 — 2026-07-20 — touch6-autoplay-golden

- Refresh Touch6 golden from **pos2 full-Dev autoplay** (`packets.raw`, TID `38D7`,
  1 card, `byte8=0x04`) — replicate still TID+seq+CRC only.
- BOM stamp tip → **2.2.2.49**.

Rollback: `2.2.2.48` (touch6-golden-replicate).

## 2.2.2.48 — 2026-07-20 — touch6-golden-replicate

- Touch6 lab synth default: **capture-replicate** copper golden
  (`lab_gameplay_templates/touch6_wager.hex` from pos2
  `2026-07-03_2212-UTC_touch6-jpot-3game-success`) — rewrite **TID + seq + CRC only**.
- Allow any 4-hex `LAB_GAMEPLAY_TID` (pilot alts: `E001`, `40CC`).
- Extract helper: `lab_gameplay_extract_touch6.py` from full-Dev `packets.raw`.
- BOM stamp tip → **2.2.2.48**.

Rollback: `2.2.2.47` (lab-gameplay-msg-fix).

## 2.2.2.47 — 2026-07-20 — lab-gameplay-msg-fix

- Fix `lab_gameplay_synth_wager` emit: duplicate `msg=` kwarg crashed the synth
  thread after the first UART→ETH forge (no further rounds).
- BOM stamp tip → **2.2.2.47**.

Rollback: `2.2.2.46` (lab-gameplay-synth).

## 2.2.2.46 — 2026-07-20 — lab-gameplay-synth

- **Lab synthetic T340+ gameplay inject** (bus-load soak): `lab_gameplay_synth.py`
  forges slave wager frames as UART→ETH origin on a reserved lab TID (`E000`–`E0FF`).
  Rotating ~5 min game sessions; within session bet tiers min→medium→high from
  `Game_Bet_Tiers.xlsx` (25¢ credits). Catalog: touch6, superpick, bingo, poker
  (default) + keno/spinjack/triple7s. Master still serves real outcomes; local
  `0D` synth unchanged. Orthogonal to Master JPOT egress hijacks.
- Env: `LAB_GAMEPLAY_SYNTH`, `LAB_GAMEPLAY_TID`, `LAB_GAMEPLAY_GAMES`,
  `LAB_GAMEPLAY_SESSION_S`, etc. Prod hard-refuse unless
  `LAB_GAMEPLAY_SYNTH_ALLOW_PROD=1`.
- Dev Lite events: `lab_gameplay_session_start|end`, `synth_wager`, `synth_skip`.
- Docs: `LAB_GAMEPLAY_SYNTH.md`.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.46** (tip align;
  no uartd behavior change).

Rollback: `2.2.2.45` (dev-light-outcome-eth).

## 2.2.2.45 — 2026-07-20 — dev-light-outcome-eth

- **Dev-light soak crumbs** (WiFi-edge / MRT eval gaps): keep Lite overnight, add thin
  gameplay wire visibility without full Dev / `packets.raw`.
  - Allow `eth_tx` + `eth_to_uart_done` in Dev Lite for wager / outcome / x1 / `0D`
    (still suppress `0A` flood).
  - `inject_peer_early_eth` now includes `peer_msgs`.
  - New `peer_gameplay_eth_fwd`: each gameplay response/ACK/`0D` in early-forwarded
    inject peer (first Master outcome TX path after `50` inject).
  - UART→ETH gameplay `eth_tx` adds `outcome_retry` + `retry_gap_ms` when same
    TID+seq+msg reappears within 2.5 s (Master `59` resent).
- Python-only — no uartd rebuild required for behavior; BOM stamp tip align.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.45**.

Rollback: `2.2.2.44` (eth-thread-ipc-timeout).

## 2.2.2.44 — 2026-07-19 — eth-thread-ipc-timeout

- **Pos4 lockup fix:** eth→UART threads no longer die on exceptions; log and continue.
- `eth_rx_queued` uses **cached** SLV-IN (no blocking IPC) for the idle diagnostic field.
- `UartDaemonClient.slv_in_active`: on IPC stall/timeout return cache instead of raising
  `TimeoutError` (pos4 `.43`: dual eth thread killed → Game 4 `80` retry storm).
- Keep `.43` infer @100 ms + `.42` orphan gate + `.41` harvest.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.44**.

Rollback: `2.2.2.43` (infer-retry-100ms).

## 2.2.2.43 — 2026-07-19 — infer-retry-100ms

- **Infer after same-seq x9 retry:** drop miss-backstop from **250 ms → 100 ms**
  (near empirical slave MRT window). Harvested local x1 still schedules synth `0D`
  immediately (no infer timer).
- Keep `.42` orphan `solo_x1` gate + `.41` harvest stack.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.43**.

Rollback: `2.2.2.42` (await-gate-infer-retry).

## 2.2.2.42 — 2026-07-19 — await-gate-infer-retry

- **Orphan solo_x1 gate (pos1 20:24 MRT):** UART `solo_x1` / glued harvest only marks
  pending when `await_x1`, `await_0d`, or late-miss `logged_x1` is open. Blocks second
  synth `0D` after `seq_already_closed` (`status_0d_synth_skip reason=no_open_await`).
- **Infer after same-seq x9 retry (pos1 overnight T2):** if `x9_retry_seen` still has
  no local x1 by **250 ms**, allow infer (`after_x9_retry=true`) instead of waiting for
  the 3 s `await_local_x1_timeout`. 125 ms no-retry infer unchanged.
- Keep `.41` harvest stack (Class A/B/D). Infer still present as backstop.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.42**.

Rollback: `2.2.2.41` (x1-harvest-ipc-queue).

## 2.2.2.41 — 2026-07-18 — x1-harvest-ipc-queue

- **Class A:** `UartDaemonClient` side-queues `WINDOW`/`HS_*`/`DISCARD` while `inject()` (and other RPC waiters) hold the socket — drained via `poll()`/`drain_events()` after DONE so pre-inject flush x1 is not silent-dropped.
- **Class B:** `pog422_uartd` re-arms 25 ms post-x9 grace after `flush_post_de_rx` so grace no longer expires during the post-DE poll window.
- **Class D:** mismatch peel finds embedded self-echo substring; peer local-x1 prefers TID-checked packets; `mrt_x1_after_x9` logs `msg`+`trigger`; new `x1_harvest_path` event (`uart_window|post_de_peel|grace|infer`).
- Instrumentation: stderr `dttty_sink=…` + `post_de_vs_hs_steal`; DEV_LIGHT adds harvest/grace/peer events. Infer **kept**.
- late_0d / await_0d abandon clears `_status_0d_synth_sent`.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.41**.

Rollback: `2.2.2.40` (narrow-tx-lock-fast-0d).

## 2.2.2.40 — 2026-07-18 — narrow-tx-lock-fast-0d

- **Keep** `.39` x1-infer (not removed until harvest root cause is gone).
- Fix pos2 08:04 EDT late-synth MRT class: `_tx_lock` now covers **uartd.inject only**;
  peer parse / x1 mark / remember / packet log / synth run **after** lock release so
  closing `0D` is not stalled behind inject epilogue I/O.
- Log `status_0d_synth_deferred_tx_lock` when synth is queued because lock is held.
- Log `mrt_x9_inject_slow` when inject_total_us ≥ 100 ms (forensics).
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.40**.

Rollback: `2.2.2.39` (x1-infer-no-x9-retry).

## 2.2.2.39 — 2026-07-17 — x1-infer-no-x9-retry

- After armed local x9: if Master does **not** re-send same-seq x9 within **125 ms**, infer that local x1 was sent (we missed harvest) and synth close — `mrt_x1_inferred_no_x9_retry` / `status_0d_synth_trigger trigger=no_x9_retry`.
- Same-seq Master x9 while awaiting x1 (`await_x1_same_seq`) marks retry seen → **no** infer; keep waiting for real x1 or 3 s `await_local_x1_timeout`.
- Fixes pos4 soak class: copper had `81`, bridge mismatch-echo miss, Master retry only at ~2 s (after slave MRT window).
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.39**.

Rollback: `2.2.2.38` (tx-gate-cleanup).

## 2.2.2.38 — 2026-07-17 — tx-gate-cleanup

- Real transaction-gate cleanup after overnight soak (pos1 late close + pos4 x9/no-x1 with defer queues >1000):
  - Cap `_transaction_deferred` at **64**; drop oldest on overflow (`transaction_defer_abandon` / `queue_cap`).
  - On `x9_no_local_x1` (3 s): emit `await_local_x1_timeout`, clear await_x1, close local trunk, **drop** deferred foreign (no flush storm). Late machine x1 can still arm await_0d via `logged_x1`.
  - Stale open trunk rounds abandon after **8 s** (`trunk_round_stale_abandon`) so `game_transaction_gate` / 0A cannot stick overnight.
- Keep `.36/.37` defer/skip log throttling.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.38**.

Rollback: `2.2.2.37` (relay-skip-log-throttle).

## 2.2.2.37 — 2026-07-16 — relay-skip-log-throttle

- Rate-limit `status_0d_relay_synth_skip` (`local_same_game_active`) to ≤1/s. Guard behavior unchanged — expected under multi-seat same-game soak (foreign 50/59/51/80/89/81 for the same game while local TID already has state).
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.37**.

Rollback: `2.2.2.36` (tx-defer-log-throttle).

## 2.2.2.36 — 2026-07-16 — tx-defer-log-throttle

- Rate-limit `transaction_defer` console/dev-light logs to ≤1/s per reason (`game_transaction_gate`, `await_local_0d`, `0a_latest_wins`). Defer queue behavior unchanged — only log spam under multi-seat soak.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.36**.

Rollback: `2.2.2.35` (x1-synth-nonblocking).

## 2.2.2.35 — 2026-07-16 — x1-synth-nonblocking

- Fix residual pos5 MRT on `.34`: UART local-x1 synth no longer calls `send_frame` while eth→UART holds `_tx_lock` (that blocked `capture_tick` ~900 ms and starved late-0D / retries). Queue P0 `status_0d_synth` / `_synth_retry_rks` instead; drain after lock release and every capture tick.
- Every `_mrt_risk_tick`: re-arm `x1_pending` for any local `await_0d` and retry synth (heals await_passthrough until x9 hint arrives, and note-without-schedule races).
- `superseded_seq`: if local `last_x9` already matches `latest_seq`, refresh hint and continue instead of abandoning.
- Keep `x1_pending` until synth inject succeeds; in-flight queued synth is not treated as `already_sent` abandon.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.35**.

Rollback: `2.2.2.34` (local-x1-synth-guard).

## 2.2.2.34 — 2026-07-16 — local-x1-synth-guard

- Force local x1 close handling to mark `x1_pending` and schedule synth `0x0D` even when the x1 is seen in `peer_x1_in_echo`; the actual inject is deferred until the tx lock releases so the close cannot silently wait for a later Master retry.
- Guard same-game foreign relay synth: when the local seat already has state for that game (e.g. pos1 and pos5 both playing Jacks or Better), foreign TID x9/x1 no longer rekeys into the local `(tid, game)` close state.
- Dev-light now logs `status_0d_synth_trigger`, `local_round_superseded`, `status_0d_relay_synth_skip`, and `status_0d_synth_skip` reasons `superseded_seq` / `await_passthrough`.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.34**.

Rollback: `2.2.2.33` (inject-tip-match).

## 2.2.2.33 — 2026-07-16 — inject-tip-match

- Tighten policy after pos5 twice stayed on uartd **2.2.2.29** while Python was **2.2.2.32**: `update.sh` rebuilds inject/uartd when binary ≠ `EG_VERSION`; `pog422_ipc.h` must match tip (FATAL if stale); `session_start` `uartd_version_ok` / `inject_version_ok` require exact tip.
- BOM stamp: `pog422_ipc.h` + `pog422_uart_client.py` → **2.2.2.33**.
- XR driver still uses the component range floor.

Rollback: `2.2.2.32` (await-0d-local-only).

## 2.2.2.32 — 2026-07-16 — await-0d-local-only

- Fix CGS Master-not-responding from 2.2.2.31: foreign solo `x1` armed sticky `await_0d`, locking absolute P0 defer of SCR/STOM. Arm close-wait only for local TID; abandon on synth-fail / late-0D timeout; ignore non-local keys in `_local_close_pending`.
- New event: `mrt_risk_await_0d_abandon`.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.31` (await-0d-priority-dev-light).

## 2.2.2.31 — 2026-07-16 — await-0d-priority-dev-light

- Absolute P0 priority for local synth `0x0D`: while `await_0d` / `status_0d_synth` queued, defer **all** other eth→UART (foreign TIDs, STOM/SCR) so close is never stuck behind relay inject.
- Dev-light logging (`FIXTURE_DEV_LIGHT` / `--dev-light`): fault + MRT/synth `events.jsonl`; no `packets.raw`. Soak default on `run_bridge_jun11_good.sh`. Skip debug log rotate on inject hot path.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.30` (early-x1-eth-forward).

## 2.2.2.25 — 2026-07-14 — empty-echo-reinject-no-arm

- Local eth→UART `x9` with empty inject echo + empty post-DE: one immediate re-inject (`mrt_x9_empty_echo_reinject`); do **not** arm the 400 ms `x9_no_local_x1` miss timer or post-x9 grace/soft-hold until bus RX proves delivery (`mrt_risk_arm_skip reason=empty_echo` if still mute).
- Far-bus listen missing `71` ≠ T340+ silent; without a tap on each Pi↔T340 local drop, swallow vs never-sent is open. Prior pos2/pos5 misses: ACK was sent, trimmed on UART→ETH harvest.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.24` (mrt-x9-x1-inject-diag).

## 2.2.2.24 — 2026-07-14 — mrt-x9-x1-inject-diag

- Focused soak logging for hard-miss vs late local `x1` after eth→UART `x9` (79→71): compact `mrt_x9_inject_diag` (echo/post-DE/peer + arm result), `mrt_x1_after_x9` with `x9_to_x1_ms` + `late_after_miss`, and enriched `x9_no_local_x1` with last inject snapshot.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.23` (mrt-arm-skip-closed-seq).

## 2.2.2.23 — 2026-07-14 — mrt-arm-skip-closed-seq

- Pos1 soak false `x9_no_local_x1` @400 ms: Master poll retry eth→UART `79` re-armed await after synth `0x0D` already completed; copper witness already had local `71`+`0D`.
- **`_mrt_risk_arm_after_local_x9`:** skip re-arm when `play_sequence` already closed (`mrt_risk_arm_skip` `seq_already_closed`), when await-0D is active, or when await-x1 already armed for the same seq (do not reset the 400 ms clock). Closing `0x0D` records `_mrt_risk_close_done_seq`.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.22` (listen-ipc-desync-seq-wrap).

## 2.2.2.22 — 2026-07-14 — listen-ipc-desync-seq-wrap

- **Listen crash (pos2 / pos51):** after header commit, non-blocking empty payload `recv` raised `BlockingIOError` → `uartd_ipc_retry` without consuming payload → next poll treated payload as magic → `RuntimeError: bad ipc magic` killed Python. Poll now waits for payload; desync disconnects + `reconnect()`; emit `uartd_ipc_desync` / `uartd_ipc_reconnect`.
- **`hat_fixture_listen.sh`:** `LISTEN_AUTO_RESTART=1` (default) restarts Python+uartd on non-zero exit **without** re-wiping capture (`CLEAR_CAPTURE` wipe is first start only). Ctrl-C / SIGTERM do not restart. Tracebacks always append to `fixture_listen.log`.
- **MRT regression (2.2.2.21):** `play_sequence` wrap/reset (soak `7→0`) left `latest_seq` high → `status_0d_synth_skip reason=superseded_seq` → `mrt_risk_late_0d_after_x1`. Supersede / synth guard now treat any **changed** seq (including decrease) as newer; same seq still leaves wait alone.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.21` (local-seq-supersede-close).

## 2.2.2.21 — 2026-07-14 — local-seq-supersede-close

- Stuck local close-wait (await `x1` / synth `0x0D` / trunk gate) could leave Contacting Game Server while the bridge process stayed up — new local wager/`x9` did not fully abandon prior close-pending state.
- **Local TID only:** for the same `(tid, game)`, any newer `play_sequence` (`> N`, not only `N+1`) supersedes the open wait; missing sequence is latest-wins. Same or lower sequence leaves the wait alone.
- Clears await-x1 / await-0D / `x1_pending` / synth-sent / skill bits; closes older/unknown trunk and flushes deferred injects. Emits `local_round_superseded`. Synth skip reason `superseded_seq` if x9 hint seq is stale.
- Python-only — no uartd rebuild.

Rollback: `2.2.2.20` (listen-ipc-eagain-retry).

## 2.2.2.20 — 2026-07-14 — listen-ipc-eagain-retry

- Listen witnesses (pos2 / pos51) died mid-soak with `BlockingIOError: [Errno 11] Resource temporarily unavailable` in `uartd` non-blocking drain: IPC header arrived, payload not ready yet → uncaught EAGAIN killed Python.
- **`pog422_uart_client._recv_exact`:** empty-socket EAGAIN still means “no event”; mid-frame EAGAIN waits (select/timeout) for the rest.
- **`listen_loop_uartd`:** catch transient IPC/socket errors, emit `uartd_ipc_retry`, continue (no process exit).
- Python-only — no uartd rebuild required for this fix.

Rollback: `2.2.2.19` (post-de-await-local-x1).

## 2.2.2.19 — 2026-07-14 — post-de-await-local-x1

- Fix pos5 MRT (witness `51` soak): after local eth→UART `x9`, post-DE harvest took only orphan SOH (`0x05`) of the slave `x1` (5 ms + 200 µs idle), then `window_skip split_empty` discarded it and grace skipped the remainder → `x9_no_local_x1` while copper `x1` existed.
- **uartd:** post-DE poll **25 ms** (match grace); idle-exit only on complete SOH…EOP frame; lone SOH held for grace/HS glue (`post_de_soh_into_hs_window`).
- **bridge/capture:** incomplete post-DE peer → `post_de_soh_hold` (no finalize skip); glue onto next WINDOW.
- Rebuild **uartd** on the Pi (`inject-shim`) — Python alone is not enough for the harvest timing.

Rollback: `2.2.2.18` (await-x1-hold-rate-limit).

## 2.2.2.18 — 2026-07-13 — await-x1-hold-rate-limit

- Fix 2.2.2.17 soak lockup: forever `await_local_x1` deferred every poll → ~80k events / 25 MB rotate spam; eth→UART wedged on D249/9309.
- Non-local eth→UART hold after local `x9` is now **2 s** (`MRT_LOCAL_X1_INJECT_HOLD_MS`), not forever; round still stays open for late `x1`→synth `0x0D` until `x1` or new local wager.
- Rate-limit `inject_deferred` logs to ≤1/s per reason.

Rollback: `2.2.2.17` (await-local-x1-defer-relay).

## 2.2.2.17 — 2026-07-13 — await-local-x1-defer-relay

- After local-TID `x9` eth→UART inject, **hold non-local eth→UART injects** until local `x1` is seen (`inject_deferred` reason `await_local_x1`).
- No wall-clock abandon: wait clears only on local `x1` (then synth `0x0D` as before) or a **new local wager**.
- Matches native wedge robustness: leave the POG422 bus free for the T340+ ACK after we delivered Master `x9` (pos5/51 MRT soak: late copper `x1` was lost under foreign relay inject).

Rollback: `2.2.2.16` (listen-uart-busy-diag).

## 2.2.2.16 — 2026-07-13 — listen-uart-busy-diag

- When HAT UART is already open, listen/uartd prints actionable busy diagnostics: `fuser`, matching bridge/supervisor processes, `fixture-prod` / `ethergold` unit state, and stop hints.
- Clarifies this failure is **port ownership**, not a bad `FIXTURE_POS`.

Rollback: `2.2.2.15` (fixture-pos-1-99-witness).

## 2.2.2.15 — 2026-07-13 — fixture-pos-1-99-witness

- Widen `FIXTURE_POS` / `--fixture-pos` from seat-only (`1–5` / listen `1–4`) to **`1..99`**.
- Allows POG422 copper witnesses without clobbering seat landings (e.g. **`51`** + `PI_LABEL=topbox5w` for pos5 listen-only).
- PC archive/normalize discover numeric `logs/N` folders (not hard-coded `1..5`).

Rollback: `2.2.2.14` (clear-capture-wipe-dirs).

## 2.2.2.14 — 2026-07-13 — clear-capture-wipe-dirs

- `CLEAR_CAPTURE=1` (default bench): wipe **all contents** of `data/` and `logs/` before start (rotated `events.jsonl.*` / `packets.raw.*` included), not only the active allowlist.
- Listen matches bridge: shell wipe first, then `--append-capture` so Python does not re-wipe `logs/` after uartd opens `uartd.log`.
- Fixes fat Dropbox re-sync of multi-day rotated chunks after each soak.

Rollback: `2.2.2.13` (mrt-means-late-0d-after-x1).

## 2.2.2.13 — 2026-07-13 — mrt-means-late-0d-after-x1

- Clarify diagnostics: **true MRT** = local x1 (`0x41`…) without timely closing `0x0D`. Log `mrt_risk_late_0d_after_x1` only for that.
- Rename missing-ack watch to `x9_no_local_x1` (rejected/ignored x9 / re-wager) — **not** MRT.
- Retest `…_1923-UTC_pos5-keno-mrt-retest-22211`: 14/14 local `0x41` → synth `0x0D` in 29–47 ms; prior `mrt_risk_missing_x1` events were x9-not-accepted, not MRT.

Rollback: `2.2.2.12` (early-win-after-tx-lock).

## 2.2.2.12 — 2026-07-13 — early-win-after-tx-lock

- Move Phase1/2 early-win inspect (incl. Keno fireworks IPC write) **outside** `_tx_lock`, after pending status `0x0D` synth — so a slow fireworks file write cannot block `41→0D` inject.
- Retest archive `2026-07-13_1923-UTC_pos5-keno-mrt-retest-22211`: fireworks was **not** causal for the two `mrt_risk_missing_x1` rounds (both `qualifies=False`); uartd grace absorb is working on good rounds. Remaining MRT = no local `0x41` (game retried `0x40` / bank traffic), not late synth after x1.

Rollback: `2.2.2.11` (uartd-post-x9-grace-absorb).

## 2.2.2.11 — 2026-07-13 — uartd-post-x9-grace-absorb

- **Keno MRT:** uartd now polls the UART FIFO for 25 ms after eth→UART inject ends with SLV-HS low (port of pyserial `post_x9_inject_grace_hold` / `absorb`). Split/local `0x41` can become a WINDOW so `status_0d_synth` still fires — **does not** synth `0x0D` before local x1.
- Python: `mrt_risk_missing_x1` / `mrt_risk_late_0d` after local x9; uartd WINDOW path applies handoff x1 filter + logs `post_x9_inject_grace_absorb`.
- BOM: `POG422_EG_VERSION` / uartd+inject → **2.2.2.11** (source-newer rebuild). Driver may stay in `[2.2.2.7, tip]`.

Rollback: `2.2.2.10` (component-version-range).

## 2.2.2.10 — 2026-07-13 — component-version-range

- Version checks use **`[EG_COMPONENT_MIN_VERSION, EG_COMPONENT_MAX_VERSION]`** (now `[2.2.2.7, 2.2.2.10]`), not identical stamps across Python / uartd / inject / XR driver.
- `update.sh` rebuilds inject/driver only when out of range (or FORCE_* / HW_DEBUG / newer sources); IPC lag vs Python tip is a NOTE, not FATAL.
- Reverted temporary C/ipc/`MODULE_VERSION` stamp from 2.2.2.9 back to **2.2.2.7** (last real binary change). Python tip may spin without forcing rebuilds.

Rollback: `2.2.2.9` (keno-balls-offset-21-raw).

## 2.2.2.9 — 2026-07-13 — keno-balls-offset-21-raw

- Keno `0x49` draw balls: **fixed offset 21**, raw display 1–80 (lab + natural).
- Removed dual-path / wire−2@19 fallacy; lab `build_keno_jpot_synth` writes raw @21.
- Phase 2 TouchEasy decode matches that single layout.
- BOM stamp: `pog422_ipc.h` / XR `MODULE_VERSION` / `install_pi.sh` → **2.2.2.9** (clears update.sh mismatch after Python-only spin; superseded by 2.2.2.10 range policy).

Rollback: `2.2.2.8` (phase2-toucheasy-keno).

## 2.2.2.8 — 2026-07-13 — phase2-toucheasy-keno

- Phase 2 cash early win for **TouchEasy Keno (9309)**: cache `0x40` picks/bet, evaluate
  `0x49` vs SGVI sheet-10 paytable, local-TID fireworks when win ≥ threshold.
- Skip when `progwinval≠0` (Phase 1 owns progressive).
- Enable: `FIXTURE_EARLY_WIN_PHASE2_KENO=1` / `sync/enable_early_win_phase2_keno_env.sh`.

Rollback: `2.2.2.7` (partial-0e-wire-len-598).

## 2.2.2.7 — 2026-07-13 — partial-0e-wire-len-598

- `COMMAND_WIRE_LEN[0x0E]=598` (Partial Master reply); alt **397** (Jun 19 census mid-EOP).
- UARTD C matched. STOM priority includes `0x0E`.
- Fixes pos3 `eth_tx_skip wire_len_crop` after Master answered Partial (`…1626-UTC_partial-2226-master-replied`): HS `05,0E…` @598 mis-parsed as `00`@131.

Rollback: `2.2.2.6` (partial-04-wire-len-32).

## 2.2.2.6 — 2026-07-13 — partial-04-wire-len-32

- `COMMAND_WIRE_LEN[0x04]=32` (Partial Transfer request); UARTD C matched.
- STOM priority/request sets include `0x04` (was documented as `0x02` only).
- Fixes pos4 `eth_tx_skip wire_len_crop` on Partial — Master never saw request (session `…1515-UTC_mtc-2225-2ok-partial-fail`).
- Full MTC `01`/`09` already OK on 2.2.2.5.

Rollback: `2.2.2.5` (mtc-inject-chunk-writesize).

## 2.2.2.5 — 2026-07-13 — mtc-inject-chunk-writesize

- `INJECT_TX`: chunk wide USB OUT to `writesize` (DE held across chunks); scale chunk timeout with air time.
- Inject lib: `timeout_ms = max(500, air_ms+500)` for large frames.
- Bridge: log inject forensics on `uartd_inject` error (`usb_actual`, FIFOs).
- Fixes MTC after 2.2.2.4: `0x09`@1170 accepted then `errno=110` ETIMEDOUT (~160 ms) on single 2340-byte URB.

Rollback: `2.2.2.4` (mtc-inject-frame-max-2048).

## 2.2.2.4 — 2026-07-13 — mtc-inject-frame-max-2048

- `POG422_INJECT_FRAME_MAX` **256 → 2048** (XR `INJECT_TX` + inject lib check).
- Fixes MTC after wire-len parse: `0x09`@1170 reached eth→UART then failed `uartd_inject errno=22` (EINVAL).
- Session proof: `…1444-UTC_mtc-2223-still-fail-no-retry`.

Rollback: `2.2.2.3` (mtc-09-wire-len-1170).

## 2.2.2.3 — 2026-07-13 — mtc-09-wire-len-1170

- Transfer Master Config: live Master `0x09` HS window is **1170 B** (pos2/3 FIFO `05,09…` + EOP), not legacy `comlengs[9]=1094`.
- `COMMAND_WIRE_LEN[0x09]=1170`; alt **1094** retained. UARTD C primary/alts matched.
- Fixes MTC reply loss: prior parse fell through to junk `00`@235 → `eth_tx_skip wire_len_crop`.

Rollback: `2.2.2.2` (peer-pkt-complete-hooks).

## 2.2.2.2 — 2026-07-12 — peer-pkt-complete-hooks

- After eth→UART inject, peeled peer TxD runs **`finalize_handshake_window_for_channel`** (outside TX lock): `packet_complete` hooks + UART→ETH — same as native HS capture.
- Fixes pos4 MRT: glued local `x1` (e.g. `61` after `69`) now arms `status_0d_synth` under `status_0d_block_all`.

Rollback: `2.2.2.1` (inject-peer-uart-eth).

## 2.2.2.1 — 2026-07-12 — inject-peer-uart-eth

- Bridge 101: POG422-origin bytes that land in the **inject RX window** (today only `inject_echo`, then cleared) are peeled after self-echo and **UART→ETH on both ports** — all opcodes, not SCR-only.
- Helper `peer_uart_bytes_after_inject()`; event `inject_peer_uart_eth` for bench proof.
- Fixes Master `0x0B` (and any peer TxD) swallowed during eth→UART inject so slaves receive the reply.

Rollback: `2.2.1.17` (scr-transaction-path).

## 2.2.1.17 — 2026-07-12 — scr-transaction-path

- Bridge: **0x0B** (Master SCR enroll reply) joins STOM comm **priority** set — bypasses `game_transaction_gate` and `post_x9_grace_*` inject defer; head-of-line queue like 0x03.
- `inject_echo`: **prefix match** scores `match=True` when bus echo = sent frame + trailing bytes (glued SCR + `0x0B` capture artifact).
- `inject_echo.jsonl`: new `match_kind` / `post_de_rx_match_kind` (`exact` | `prefix` | `mismatch` | `none`).
- BOM stamp: inject/uartd/`MODULE_VERSION` → **2.2.1.17** (driver path unchanged; clears update.sh mismatch).

Rollback: `2.2.1.16` (wide-rx-9bit-flag0).

## 2.2.1.16 — 2026-07-12 — wide-rx-9bit-flag0

- UARTD: open port as **CS5 → 9-bit** (Exar quirk) so wide RX flag **bit0 = 9th data bit**, not 8-bit wide “parity error.”
- Explains 2.2.1.15 session: native SCR SOH showed `05/00` in `wide_rx.bin` while bank accepted — instrumentation was in 8-bit+wide.
- Soft HS flag bit `0x10` unchanged. DTtty still delivers the low 8 data bits only.

Rollback: `2.2.1.15` (wide-rx-tee).

## 2.2.1.15 — 2026-07-12 — wide-rx-tee

- Driver: parallel **wide RX tee** ring — every preciseflags pair kept as `[data][flags]`; unused RAMCTL bit **`0x10`** = cached SLV-IN/HS (`last_slv_in_active`).
- New ioctls: `SET/GET/READ/FLUSH_WIDE_RX` (does not change DTtty protocol path).
- UARTD: optional always-on thread (`POG422_WIDE_LOG=1`) drains tee to `${OUTPUT_DIR}/wide_rx.bin` (same wide layout).
- Purpose: prove MARK bit / HS state on copper vs 8-bit strip into `packets.raw`.

Rollback: `2.2.1.14` (slv-in-sample-restore).

## 2.2.1.14 — 2026-07-12 — slv-in-sample-restore

- Inject: restore **cheap** `slv_in_phase_active` samples while DE is high (`after_on`/`body` after bulk, `after_drain` before GPIO7 off).
- Fixes false `slv_hs_misalign` storm from 2.2.1.13 (fields left zero after dropping copper_push).
- Still no multi-register `copper_push` / FIFO get_reg on the DE-high path (hold stretch).

Rollback: `2.2.1.13` (ioctl-de-defer).

## 2.2.1.13 — 2026-07-12 — ioctl-de-defer

- `INJECT_TX` MANUAL_DE: assert GPIO7 **inside** the ioctl after mutex/pm, immediately before bulk (userspace no longer asserts first).
- Skip FIFO get_reg before/after bulk on MANUAL_DE path (USB controls were stretching DE-high before ioctl returned).
- Userspace: hybrid air/TEMT wait from estimated DE assert (`tx_ns`); no copper sampling while DE high.
- Goal: `de_hold_us` ≈ air+margin (~6.5 ms SCR) instead of ~17 ms ioctl dwell.

Rollback: `2.2.1.12` (hybrid-de-deadline).

## 2.2.1.12 — 2026-07-12 — hybrid-de-deadline

- Inject: DE release = **min(TEMT, air_us + post_hold margin)** from GPIO7 assert.
- Default margin **400 µs** (`POST_AIR_MARGIN_US`); `pre_tx` stays **0**.
- New `de_release_reason=deadline` (4); TEMT/EN_ACT still logged as forensics.
- Short WAIT slices (≤5 ms) so air deadline is not overshot.
- Goal: shrink Master SCR inject HS/~25 ms `de_hold` toward native ~7 ms / ~6.1 ms air.

Rollback: `2.2.1.11` (fall-stamp-clamp).

## 2.2.1.11 — 2026-07-12 — fall-stamp-clamp

- UARTD: fall-burst / end-window clamp byte stamps to ≤ HS-fall time (FIFO drain is not post-HS copper).
- Analysis note (not a code delay): Master SCR inject `de_hold_us` ≈ **22–30 ms** waiting **TEMT/EN_ACT** — matches pos2 `hs_width_us`. `pre_tx`/`post_hold` remain 0; long “HS after last byte” is DE held for late TX-done, not a post-inject sleep.

Rollback: `2.2.1.10` (hs-porch-stream).

## 2.2.1.10 — 2026-07-12 — hs-porch-stream

- `HW_DEBUG=1` UARTD: stream-drain RX **while HS is high** (prod still burst-on-fall) so `first_byte` / `last_byte` mono are real.
- Events: `hs_porch` + porch fields on `packet_complete` — `front_porch_us`, `byte_span_us`, `back_porch_us`, `hs_width_us`, plus `t_hs_rise/first/last/fall_perf_ns`.

Rollback: `2.2.1.9` (listen-session-start-fix).

## 2.2.1.9 — 2026-07-12 — listen-session-start-fix

- Fix UARTD listen crash: `session_start` passed `uartd_version=` and `**hat_probe` (duplicate kwarg) → TypeError before any capture (empty `events.jsonl`).
- `uartd_common.sh` stop: SIGKILL after ~1s if uartd stuck in USB `skb_wait` so Ctrl-C returns a prompt.

Rollback: `2.2.1.8` (uartd-listen).

## 2.2.1.8 — 2026-07-12 — uartd-listen

- Restore **UARTD listen** on the flat EG stack: `hat_fixture_listen.sh` / `run_listen_jun11_good.sh` start `pog422_uartd` (inject=0) so pos1/2 witnesses use the same C HS/RX path as bridge, including `HW_DEBUG=1` → `hw_debug.jsonl`.
- `uartd_common.sh` passes `HW_DEBUG` through to `make` (bench rebuilds keep the dense timeline).
- Prod listen no longer refuses a running uartd — listen owns the daemon.

Rollback: `2.2.1.7` (custom-driver-en-act-txdone).

## 2.2.1.7 — 2026-07-11 — custom-driver-en-act-txdone

- Fix B1422 `init_xr21b142x_reg_map`: `CUSTOM_DRIVER` (0x060) was written onto the **140x** map — probe skipped bit0=1. Now set on 142x map before GPIO/`CUSTOMIZED_INT`.
- TX-done naming: B1422 has no Tx-empty USB IRQ / no LSR. `WAIT_TX_EMPTY` returns `reason` (`already_idle` / `en_act_fall` / …); inject gates DE-off on `temt` only (no fake LSR bit6).
- Keep `FLOW_CONTROL` bit3 clear (UART Half-Duplex); `hat_init` clears it if set.
- Docs: `CUSTOM_DRIVER` ≠ `CUSTOMIZED_INT`; HS=GPIOB1; GPIO5=EN_ACT sense only.

Rollback: `2.2.1.6` (hw-debug-compile).

## 2.2.1.6 — 2026-07-11 — hw-debug-compile

- Compile-out `POG422_HW_DEBUG` timeline (bench `make HW_DEBUG=1`; prod default off — no runtime `is_debug`).
- Userspace: `pog422_hw_debug.h` ring; uartd HS/RX samples + IPC `HWDBG` → `hw_debug.jsonl`.
- Inject: `copper_push` GPIO_STAT/FIFO polls **only** when `HW_DEBUG=1`; denser mid-TEMT copper under debug.
- Driver: Table-8 USB INT edge ring (GPIO1 + GPIO5 sense) + `DRAIN_HWDBG` ioctl — GPIO5 **only** from that USB path.
- `session_start.hw_debug` from daemon version reply (compile-time capability bit).

Rollback: `2.2.1.5` (no-pre-tx).

## 2.2.1.5 — 2026-07-11 — no-pre-tx

- Eliminate pre-TX settle: C ignores `opts->pre_tx_us` (GPIO7 assert → immediate `INJECT_TX`).
- Launcher/BridgeTiming defaults: `PRE_TX_US=0` (was **3000** on uartd inject — HS→first-byte lead).
- Bridge `_inject_pre_tx_us` always returns 0; field kept for log/IPC compat only.
- Pair with 2.2.1.4 (no post-hold): DE envelope is assert → bulk → TEMT release only.

Rollback: `2.2.1.4` (no-post-hold).

## 2.2.1.4 — 2026-07-11 — no-post-hold

- Eliminate post-DE safety delay: C `sleep_us(200)` after GPIO7 release removed.
- Launcher/BridgeTiming defaults: `POST_TX_US=0`, `POST_AIR_MARGIN_US=0` (was still 1000/200 on uartd inject path — logged as `post_hold_us=1200` even though TEMT inject ignored it).
- Bridge `_inject_post_hold_us` always returns 0; field kept for log/IPC compat only.

Rollback: `2.2.1.3` (wire-len-c-sync).

## 2.2.1.3 — 2026-07-11 — wire-len-c-sync

- Sync `pog422_uartd.c` capture framing to Python `COMMAND_WIRE_LEN` / `_ALTS`.
- Fix stale C `0x20=41` (Python primary is **37**, alt 48); add missing opcodes (0x01/0x09/poker/bingo/…).
- HS capture helpers now try longest-first alt lengths (same as `split_packets`).
- Does not by itself fix pos2 CMSPAR listen vs inject SCR; that remains a separate RX-stack issue.
- **Follow-up:** `install_pi.sh` `EXPECTED_VER` was left at `2.2.1.1` while `MODULE_VERSION` was `2.2.1.3`, so a rebuild could never load the BOM-matched driver (`component_versions_ok=false` on bench). Fixed to `2.2.1.3` — re-run `install_pi.sh` on pos3/4 after stopping bridge.

Rollback: `2.2.1.2` (packets-raw-full-hs).

## 2.2.1.2 — 2026-07-11 — packets-raw-full-hs

- `packets.raw`: one hex line per HS window = **full copper buffer** (parsed or `window_skip`).
- Fixes listen on pre-v16ai builds that only wrote successful `packet_complete` lines (e.g. pos2 `v16ac` lost the 20 B inject fragments).
- `window_skip` still carries `hs_window_raw` in events; no duplicate split-packet lines for the same window.

Rollback: `2.2.1.1` (skip windows may be missing from `packets.raw` on older listen).

## 2.2.1.1 — 2026-07-11 — temt-gpio-mode-match

- Inject `pog422_hat_init` wrote `GPIO_MODE=0x0300`, stomping driver `0x030B` (GPIO5 TX-sense).
- Align inject `POG422_GPIO_MODE_MANUAL_SLV` to **`0x030B`** so USB interrupt IN TEMT can fire.
- Bench 2026-07-11_2029: 100% `de_release_reason=timeout`, DE ~air+50 ms — LNT activity stayed thick.

Rollback: `2.2.1.0` (driver OK; inject still stomps mode).

## 2.2.1.0 — 2026-07-11 — temt-usb-int-tx-sense

- `WAIT_TX_EMPTY` blocks on **USB interrupt IN** (Table-8), not FIFO poll.
- SLV: GPIO7 stays **manual DE**; GPIO5 = `RS485_EN_ACT` **sense-only** (TX activity).
- GPIO5 fall → interrupt URB → confirm `TX_FIFO_COUNT==0` → TEMT → C drops GPIO7.
- INT mask `0x03D0` (GPIO1 HS + GPIO5 TX-done). No RS485_EN_ACT on the DE pin.

Rollback: `2.2.0.0` (FIFO-poll TEMT).

## 2.2.0.0 — 2026-07-11 — temt-de-rx-harvest

- Product BOM `EG_VERSION=2.2.0.0` for bridge, uartd, inject, driver.
- Manual DE: `INJECT_TX` skips kernel air `usleep`; C waits TEMT via `WAIT_TX_EMPTY`.
- Continuous `poll_fn` harvest during DE-high; post-DE RX window tightened to 5 ms.
- Forensics: `de_hold_us`, `lsr`, `de_release_reason` (`temt` / `timeout` / `error`).
- Retires air-deadline (`pre+air+post`) DE release.

Rollback: `2.1.0.0` (uart-c-spine + inject-gate).

## 2.1.0.0 — 2026-07-11 — uart-c-spine + inject-gate

- Product BOM `EG_VERSION=2.1.0.0` for bridge, uartd, inject, driver.
- Mode C spine: default `POG422_UARTD=1` + `POG422_UARTD_INJECT=1`; MARK/SPACE Python TX hard-failed on EG2.
- C `INJECT` `require_idle` + `idle_wait_ms` — bus-idle wait inside inject burst.
- Suspend/resume no longer used for eth→UART inject; post-x9 grace armed from `INJECT_DONE`.
- `CHANNEL=both` via two uartd instances (machine + ois sockets).
- Wire-ownership doc: DATA+HS LED / RCV INT / `INJECT_TX` are success oracles.

Rollback: last pre-EG2 tip was parser `v16cb` + uartd/inject `1.0.60` + driver `1W-pog422-tx-fifo-errno-ch`.

## Legacy map (archives only)

| Legacy ID | Notes |
|-----------|--------|
| `v16cb-raw-bus-framing` / `1.0.60` / `1W-…` | Last pre-2.0 snapshot |
| `1Q`…`1W` letter drivers | Wide/preciseflags line; identity replaced by EG four-tuple |
| `v16xx` parser soup | Replaced by `EG_VERSION` + optional slug in changelog only |
