﻿# EtherGold (EG) version changelog

Format: `MajorFeature.MinorFeature.MajorFix.MinorFix` (product BOM).
**EG_VERSION** is the whole comm aggregate (HTTP, listen, pogcc-com, Python, OTA).
Any of those component changes bumps this stamp ΓÇö it is what OTA and `/health`
show. Do **not** ship HTTP letter stamps as a second product.

**UARTD_ABI_VERSION** (pog422_ipc.h `POG422_EG_VERSION`) is the last uartd/inject
ELF rebuild. `update.sh` rebuilds those binaries when they Γëá ABI, not when
Python/HTTP moves. XR driver may still fall in
`[EG_COMPONENT_MIN_VERSION, EG_COMPONENT_MAX_VERSION]`.

## 2026-09-15 ΓÇö EG **4.1.0.21** (`config-eg-busy-unlock`)

`/config` Run EG / Stop EG no longer stick permanently disabled after a hung
Update Now / promote / HTTP bounce. `field_config_ui.html` keeps a shared
`egBusy` + `egRunLocked` sync (`syncEgRunButtons`), clears busy on login /
send-to-login / OTA refresh failure, and applies `CHECK_TIMEOUT_MS` to EG
Update Now. Collector lock (`run_eg_allowed === false`) still disables both
buttons. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.20** (`fw-threshold-env-migrate`)

`install_fixture_bridge_env.sh --ensure` (already called by `update.sh`) now
idempotently rewrites leftover Phase 2 fireworks bench gates in
`~/EtherGold/fixture-bridge.env`: `FIXTURE_FIREWORKS_WIN_THRESHOLD_USD=4[.00]` ΓåÆ
`50.00`, and matching `FIXTURE_EARLY_WIN_{SLOT,KENO}_THRESHOLD_CENTS=400` ΓåÆ
`5000`. Intentional overrides (e.g. `10.00`) are left alone. Missing keys stay
missing ΓÇö code default is already $50 from **4.1.0.19**. UARTD ABI unchanged
(**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.19** (`fireworks-threshold-50`)

Phase 2 early-win fireworks default gate is now **$50.00** (was $4.00 bench
default) for **all** Phase 2 cash games ΓÇö slots (970C) and keno (TouchEasy 9309
/ SuperBall 930A). Same `FIXTURE_FIREWORKS_WIN_THRESHOLD_USD` / enable-script
`--usd` override still works for a lower bench gate. `run_bridge_jun11_good.sh`
and the Phase 2 enable scripts now default to `50.00`. Phase 1 progressive
(progwinval) unchanged. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.18** (`manual-listen-health`)

A manual listen bench no longer reads as a broken field seat. `/health` uses
`systemctl is-enabled ethergold-collector.service` as the oracle for who is
*supposed* to own listen: unit **disabled** + `pog422_listen.py` up =
`manual_listen`, `worker_owner: manual`, and neither `collector_inactive` nor
`supervisor_down` is a fault (the listen supervisor belongs to the systemd
seat). Unit **enabled** but inactive is still a hard fault, and `listen_down`
still fails either way ΓÇö a disabled unit with nothing running is not a healthy
bench. `/health` adds `collector_enabled`, `manual_listen`, `worker_owner`;
`services.collector` adds `enabled` / `worker_owner`. Health and dashboard
Collector tiles show gray **manual** ("unit disabled ΓÇö run_ethergold.sh owns
listen") instead of red **down**, and the Listen PID tile reads
`owner manual (run_ethergold.sh)`. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.17** (`run-eg-buttons`)

`/config` **Start bridge** / **Stop bridge** renamed **Run EG** / **Stop EG** ΓÇö
they call `run_ethergold.sh`, which branches on `ETHERGOLD_ROLE`, so a listen
seat starts listen (`run_listen_prod.sh`, or `run_listen_jun11_good.sh` when
`FIXTURE_PROD=0`), not a bridge. Both buttons now lock out when
`ethergold-collector.service` owns the worker: a second listen would contend for
uartd / Pi HS BCM9, and `pkill pog422_listen.py` returns in `RestartSec`.
`deploy_status()` adds `role`, `collector_active`, `worker_owner`,
`run_eg_allowed`; the card hints Recover stack instead. Bench seats with no
systemd owner (e.g. pos5 `.148`, role listen, `FIXTURE_PROD=0`) keep both
buttons live. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.16** (`ota-channel-ui`)

Show OTA channel on field UIs: `/EtherGold/config` Software OTA,
`/EtherGold/health`, and `/EtherGold/status` dashboard. Red **OTA channel: /dev**
when `OTA_CHANNEL=dev` (SGVI/dev); green **OTA channel: /prod** for field
(`OTA_CHANNEL=field`). `/health` JSON and `/EtherGold/api/runtime` now include
`ota_channel`. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.15** (`wire-bank-byte2`)

Wire **Bank Number** is byte 2 of every SOH frame (`SOH|opcode|BANK|TIDΓÇª`).
`PacketMeta.bank_number`, `packet_complete` / `eth_rx` / `eth_relay` /
`inject_echo` / SCR diag now stamp `bank` (+ `local_bank` / `bank_ok` when
configured). Foreign-bank frames still land in captures but skip UART hooks,
ethΓåÆUART inject, and `progressives.csv` (0x0A). Local bank from
`EDGE_BANK` / `FIELD_BANK` / `BANK_NUMBER` / cabinet identity; unset (0) =
accept all. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-15 ΓÇö EG **4.1.0.14** (`uartd-own-not-contention`)

Listen UART contention probe no longer treats **own** `pog422_uartd` as
`foreign_pid_holds_port` when `POG422_UARTD=1` / `--uartd` (hat_listen starts
uartd, then Python attaches via socket). Fixes Mini OHD collector restart loop
and frozen `progressives.csv` (topbox33). Tip/witness still refuse a *foreign*
collector unit when not `prod_mode`. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-14 ΓÇö EG **4.1.0.13** (`usb-replug-job-clear`)

Status USB cycle 2/2 modal loop: `finish` restarted HTTP mid-request so
`apply-job.json` never left `await_replug` (Reconnected clicked many times;
`usb_replug` itself already succeeded). Persist `finishing` before finish; skip
HTTP restart under `WEB_APPLY`; clear job on success; Dismiss calls
`/apply/usb-cancel`. UARTD ABI unchanged (**4.1.0.11**).

## 2026-09-14 ΓÇö EG **4.1.0.12** (`hat-status-led`)

POG-COM-PI-HAT **LED1** (BCM2, active-low): EtherGold POST + heartbeat.
`hat_status_led.py` / `hat_status_led.sh` ΓÇö N-count POST (100 ms pulses), healthy
250 ms, degraded 150 ms + soft-fault codes. `hat_bridge.sh` phases 1/2/4; bridge
Python takes GPIO for 5ΓÇô8 and healthy. Hardening: wait for `ttyXRUSB*` (phase 2),
uartd start/connect retries (3/4/7). Env `EG_STATUS_LED_GPIO=2`, `HAT_STATUS_LED=0`
disables. Requires `dtparam=i2c_arm=off`. UARTD ABI stays **4.1.0.11**.

## 2026-09-13 ΓÇö EG **4.1.0.10** (`fixture-dev-rotated-inotify`)

Fixture `/dev` only: inotify watcher offloads stamped capture under
`~/EtherGold/dev/{data,logs}` to `DROPBOX_DEST`, then deletes local. Hard-refuses
`/prod`. Scripts: `offload_fixture_dev_rotated.sh`, `watch_fixture_dev_rotated.sh`,
`install_fixture_dev_rotated_offload.sh` (watch service + 30 min safety timer).
Requires `inotify-tools`. UARTD ABI stays **4.1.0.9**.

## 2026-09-13 ΓÇö EG **4.1.0.9** (`w2-hs-already-active-fall`)

Pos4 overnight MRT (970D/`37BC` @ 05:44:43Z): deferred `81` on copper during
local `89` Window 2; empty harvest; same bytes scooped later on next `0A` echo.

Window 2 no longer waits up to 5 ms for HS IN *idle* after Window 1. After W1,
HS should already be inactive; W2 starts ┬╡s later. If HS is **already active**
at W2 entry, treat that as the in-flight `81` ΓÇö drain until fall and collect
FIFO (do not hunt a new rise). Idle at entry ΓåÆ wait rise ΓåÆ drain ΓåÆ fall flush.

`UARTD_ABI_VERSION` / `POG422_EG_VERSION` ΓåÆ **4.1.0.9** (uartd rebuild).

## 2026-09-12 ΓÇö EG **4.1.0.8** (`ohd-irq-affinity-quiet`)

`update.sh` printed red `Job for ohd-irq-affinity.service failed` during the
CPU-affinity step. Process pins were fine; the oneshot failed because systemd
ran `apply_irq_affinity.sh` from the Dropbox tip (CRLF `sed -i` / `set -e`).

Fix: install LF copies under `/usr/local/lib/ohd/`, CRLF heal via mktemp (no
in-tree rewrite), `set +e` + always `exit 0`, and quiet `systemctl start`.
UARTD ABI stays **4.1.0.7**.

## 2026-09-12 ΓÇö EG **4.1.0.7** (`pi-hs-gpiochip-clock-de`)

SCR `de_hold` / `pre_bulk` fluff on Pi HS:

1. **Hot path GPIO:** `pog422_pi_hs.c` uses gpiochip v2 ioctl (same as
   `pog422_gpio_hs.py` / SuperBall). `pinctrl` `system`/`popen` is init/fallback
   only ΓÇö each fork was ~1ΓÇô3 ms per DE set / IN sample.
2. **`de_hold`:** Pi HS waits `air+margin` with one `nanosleep` ΓÇö no 100 ┬╡s
   HS-sample loop. Peer HS never asserts during SCR TX (Master answers after
   we drop DE); the loop only stretched hold past deadline via pinctrl tax.

`UARTD_ABI_VERSION` / `POG422_EG_VERSION` ΓåÆ **4.1.0.7** (uartd rebuild).

## 2026-09-12 ΓÇö EG **4.1.0.6** (`install-pi-expected-from-version`)

Hotfix: `install_pi.sh` still hard-coded `EXPECTED_VER=2.2.2.11` after
XR **2.2.2.12** built cleanly ΓåÆ `FATAL: built ΓÇª version=2.2.2.12 want 2.2.2.11`.
Now reads `EXPECTED_VER` from `xr-usb-serial-pog422/VERSION`. UARTD ABI stays
**4.1.0.5**.

## 2026-09-12 ΓÇö EG **4.1.0.5** (`ext-de-no-wire-sleep`)

SCR inject wall was ~26 ms with idle_wait=0 because Pi HS paid air **twice**:

1. **XR 2.2.2.12:** `INJECT_TX` `EXTERNAL_DE` now returns after bulk like
   `MANUAL_DE` ΓÇö no kernel `usleep(air+margin)`. Prior tip slept ~air inside
   the ioctl (`bulk_wallΓëêair`, `tx_usΓëê100 ┬╡s`) then C waited again.
2. **inject C:** Pi HS DE release is one air+margin wait (BCM sample / sleep);
   no `WAIT_TX_EMPTY` USB while DE high.
3. **uartd:** drop post-inject `gpio7_rx` on Pi HS (XR GPIO7 N/C; that USB
   control was ~6 ms of SCR `uartd_post_burst`).

`UARTD_ABI_VERSION` ΓåÆ **4.1.0.5**. XR floor ΓåÆ **2.2.2.12**.

## 2026-09-12 ΓÇö EG **4.1.0.4** (`scr-allow-slave-retry`)

Spoke UARTΓåÆETH must **not** suppress Slave copper ``03`` retries while awaiting
Master ``0B``. 4.1.0.2/3 ``scr_uart_eth_inflight`` blocked star re-ask for 220 ms
after the first eth_tx ΓÇö wrong when the glass has not seen ``0B`` yet (pos2 soak
regression). Now only skip eth re-TX when same-TID ``0B`` is **queued or just
injected**. Hub ethΓåÆUART same-TID coalesce unchanged.

## 2026-09-12 ΓÇö EG **4.1.0.3** (`scr-coalesce-hook-sig`)

Hotfix: `_on_packet_complete_scr_coalesce` used the wrong hook signature
`(pkt, meta, ΓÇª)` vs required `(meta, pkt, packets_in_window, ΓÇª)`. Every copper
`packet_complete` raised and **uart_to_eth went silent** after Dropbox picked up
4.1.0.2 without a formal stop/sync. Also skip `0B:TID` keys in `_scr_awaiting_reply`.

## 2026-09-12 ΓÇö EG **4.1.0.2** (`scr-latency-shave`)

SCR latency shaves after 4.1.0.1 unicast soak (retries remained ~20ΓÇô40%/round).

- **Same-TID SCR coalesce** (`SCR_COALESCE_MS=220`): drop ethΓåÆUART duplicate
  `03` while pending / in-flight / recently answered `0B`; same for duplicate
  `0B`; spoke UARTΓåÆETH suppresses re-TX of `03` while awaiting reply.
- **`INJECT_PRIORITY_SCR=12`** beats soft `0A` / other STOM (10).
- Soft progressive **`0A` defers** while any SCR awaits Master `0B` (UART budget).
- `UARTD_ABI_VERSION` stays **4.1.0.0**.

## 2026-09-12 ΓÇö EG **4.1.0.1** (`scr-tid-unicast`)

SCR (`0x03` / `0x0B`) is **not** a bank broadcast. Remove ETH multi-spoke flood.

- Mapped SCR: TIDΓåÆgretap **unicast** (same as cash/skill).
- Hub unmapped SCR: `[]` (nowhere) ΓÇö never flood all spokes.
- Spoke unmapped SCR: uplink flood still (`None`) so Local Net Test reaches Master.
- Inject priority still treats SCR as STOM-class (queue order unchanged).
- `UARTD_ABI_VERSION` stays **4.1.0.0** (Python/routing only).

Soak driver: foreign-TID `0B` injects on spokes should drop to ~0 when TID map is warm.

## 2026-09-12 ΓÇö EG **4.1.0.0** (`pi-hs-window-harvest`)

Major refactor: Pi HS inject harvest is two HS windows, not timer peels.

- **Window 1:** assert HS OUT + TX `89` ΓåÆ drain while our HS active (self-echo
  only) ΓåÆ **release HS OUT** so T340+ Slave can take the bus.
- **Window 2:** wait HS IN rise ΓåÆ drain UART **only while HS IN active** ΓåÆ on
  fall flush once and parse local `81`. No post-inactive imm/post_de hunt.
- UART-HS path keeps legacy imm/post_de peels.
- Bridge: orphan RX + empty-harvest reinject **default off**
  (`MRT_X1_ORPHAN_DIAG`, `MRT_EMPTY_HARVEST_REINJECT`); progressive `0A` remains
  deferred until `0D` under `local_game_txn_exclusive` (WiFi star FIFO clean).
- `UARTD_ABI_VERSION` / `POG422_EG_VERSION` ΓåÆ **4.1.0.0** (uartd rebuild required).

## 2026-09-12 ΓÇö EG **4.0.0.73** (`uart-port-contention`)

Tip/workshop listen must not share XR UART with prod collector. Dual-open can
succeed while collector drains RX ΓÇö HS LEDs look alive, tip sees
`window_bytes=0` (looks like dead UART).

- `probe_uart_port_contention` / `session_start` crumbs (holders + units)
- `pog422_listen` **FATAL** when collector/bridge active or foreign PID holds
  `ttyXRUSB*` (override `ALLOW_UART_SHARE=1`)
- `hat_listen.sh` early refuse if `ethergold-collector` active
- `uartd_common` diagnose names collector

## 2026-09-12 ΓÇö EG **4.0.0.72** (`boot-config-guard`)

**Desktop/OS outranks HS-idle.** `.239` lost HDMI after hourly
`install_hs_out_idle.sh` truncated `/boot/firmware/config.txt` to only the
DYZINETEK_HS_OUT_IDLE gpio block (253 B) ΓÇö Bookworm KMS (`dtoverlay=vc4-kms-v3d`)
gone, no `/dev/dri`, lightdm crash-loop.

Fix:
- `EtherGold/sync/boot_config_guard.py` ΓÇö health gate (KMS + `arm_64bit=1` +
  min size), golden backup on ext4 `/var/lib/ethergold/boot-config-golden.txt`
  (not boot FAT), atomic write + readback verify, refuse shrink vs golden,
  `repair` restores golden over a wiped config.
- `install_hs_out_idle.sh` calls the guard for apply/remove; installs
  `ethergold-boot-config-guard.service` (sysinit, before display-manager) with
  authority to repair even if HS-idle is later uninstalled.
- DF `sync/` mirrors guard + installer (delegates to EG when present).

Never `sudo reboot`. Soft reboot on SEEED CM5 still needs hard power cycle.

## 2026-09-11 ΓÇö EG **4.0.0.71** (`mrt-x1-orphan-rx-diag`)

Diagnostic only (no product close/reinject change): after empty peel
(`peel_src=0` / empty post-DE) arm a 300 ms spoke orphan RX watch
(`post_x9_orphan_rx_arm` / `post_x9_orphan_rx` / `post_x9_orphan_x1_hit`) so a
late Slave `81` can be proven in events. Hub logs `mrt_hub_master_x9` (incl.
~125 ms `is_retry`) and `mrt_hub_spoke_x1` (ETH `81` gap since Master `89`) for
the same soak join. Disable with `MRT_X1_ORPHAN_DIAG=0`.

## 2026-09-11 ΓÇö EG **4.0.0.70** (`empty-harvest-reinject-pending-expire`)

Empty `89` peel (harvest=5) never reinjected: `pending_expire` AUTO_CLOSE_DONE
IPC does not carry inject_echo, so `echo_ok` was always False and EG60 reinject
never armed ΓÇö while Master retried ~125 ms on hub copper and spoke deferred
those retries behind `local_game_txn_exclusive`.

Fix: keep echo/post_de bytes on `_mrt_risk_x9_diag` at inject; on miss recover
`echo_ok` from diag so `_hand_open_after_auto_close_miss` lifts deferred /
queues `empty_harvest_x9_reinject`.

## 2026-09-11 ΓÇö EG **4.0.0.69** (`stale-handshake-adhoc-enroll`)

Hub was reaping spokes as `stale_handshake` in a loop (CGS on ad-hoc join).
Root cause: spoke re-enrolled every ~12s while WG `latest handshake` was still 0
(or only tens of seconds old ΓÇö normal for WireGuard), so crypto handshake never
completed; hub then dropped `handshake=0` peers after process-start grace.

Fix: per-peer enroll grace on hub reap; spoke waits 90s for first handshake;
re-kick threshold aligned with `EDGE_SPOKE_STALE_S` (~150s), not 12s.

## 2026-09-11 ΓÇö EG **4.0.0.68** (`status-health-bridge-cards`)

Status page Health strip was still listen-hardcoded (Collector red Down,
Inject always painted as ΓÇ£0ΓÇ¥). Now matches config: WiFi Hub/Spoke,
Collector gray n/a on bridge, Inject path shows live software TX flag.

## 2026-09-11 ΓÇö EG **4.0.0.67** (`health-cards-hub-spoke-inject`)

Health polish for wifi-star: Collector is gray **n/a** (not red Down) on
bridge profiles; Services show **WiFi Hub** / **WiFi Spoke** (or Side A/B)
from node role. **Inject path** = software TX flag
(`POG422_UARTD_INJECT` / `--uartd-inject`), detected from the live process ΓÇö
not a live packet counter. Listen still wants path off (0).

## 2026-09-11 ΓÇö EG **4.0.0.66** (`health-mode-by-config`)

Health cards follow field profile / `ETHERGOLD_ROLE` / `EDGE_MODE`:
**listen** ΓåÆ inject=0, listen+collector+supervisor; **bridge** ΓåÆ inject=1,
`run_ethergold` / `pog422_bridge`, no listen/collector faults. UI section
title switches Listen Γåö Bridge.

## 2026-09-11 ΓÇö EG **4.0.0.65** (`promote-tip-apply-defer-http`)

Bugfix: Web Promote restarted `ethergold-http` inside `promote_staging.sh`
**before** tip apply could rebuild inject/uartd, so Status stayed
ABI MISMATCH after Promote. With `WEB_APPLY=1`, promote defers HTTP restart;
tip-apply `finish` (or field skip path) owns the restart after rebuild/USB.

## 2026-09-11 ΓÇö EG **4.0.0.64** (`web-promote-apply`)

Web Status Promote/Update Now on **`OTA_CHANNEL=dev`** overlays Staging `prod/`
onto **`~/EtherGold/dev` without `--delete`**, then tip-applies (rebuild
inject/uartd when ABI/source requires it). USB driver cycles pause with
browser **Disconnected / Reconnected** (mirrors `update.sh` Enter prompts) ΓÇö
never silent `SKIP_DRIVER=1`. Field channel stays prod-only promote.

Status also shows tip/ABI/`tip_abi_ok`/`apply_phase`, plus **Start bridge**,
**Stop bridge** (`SKIP_SYNC=1`), **Sync capture**.

## 2026-09-11 ΓÇö EG **4.0.0.63** (`listen-until-x1`)

Empty sync peel is **not** a definitive miss. Overnight EG60 SpinBall soak
(~15% `auto_close_miss` on good `89` echo) reinjected while late `81` was still
in flight ΓÇö MRT / reinject storm class.

1. **uartd:** on good TX/echo but no x1 yet, do **not** send early
   `AUTO_CLOSE_DONE` NO_X1. Arm pending immediately. Imm peel ~100ΓÇ»ms;
   `AUTO_CLOSE_PENDING_NS` **120ΓÇ»ms** (Master re-x9 window). Pending **expire**
   sends `AUTO_CLOSE_DONE` miss once (`harvest=5` = pending_expire).
2. **Python:** sync `auto_close=None` ΓåÆ `status_0d_auto_close_pending` only
   (refresh await_x1). Miss / hand-open / EG60 reinject only on late
   `AUTO_CLOSE_DONE` (`via=pending_expire` when harvest=5).

uartd ABI **4.0.0.10** ΓÇö rebuild inject-shim on each Pi.

**Soak checklist (vs overnight baseline
`sessions/2026-09-11_1602-UTC_wlan-star-eg60-overnight-spinball-mrt2`):**

- SpinBall AutoPlay on pos1ΓÇô4
- Success: `auto_close_miss` Γë¬ prior ~15% of `89`; reinject storms rare
- Hard `await_local_x1_timeout` down; glass MRT rate down vs baseline
- Expect more `status_0d_auto_close_pending` + late ok / fewer premature miss

## 2026-09-10 ΓÇö EG **4.0.0.62** (`ohd-proxy-df`)

Public **`/ohd/*`** on ethergold-http `:31234` reverse-proxies to DyzineFrame
localhost `:31235` (setup, modes, modes-status API). Status/home/config/health
nav links. Clear 502 HTML/JSON when DF is down. Pair with DF **4.0.1.31**
(always bind local HTTP when `ETHERGOLD_HTTP_EXTERNAL=1`).

uartd ABI unchanged (**4.0.0.9**).

## 2026-09-10 ΓÇö EG **4.0.0.61** (`fast-hub-reconnect`)

Master-service power return: bank must talk again within **Γëñ5ΓÇ»s** once the hub
is up (spokes may stay running).

1. **WG PersistentKeepalive** default **5ΓÇ»s** (`EDGE_WG_KEEPALIVE_S`, was 25) ΓÇö
   spoke conf, live `wg set`, hub peers, thumb/provision templates. Bridge start
   bumps live peers so old on-disk `= 25` does not wait a full 25ΓÇ»s.
2. **Spoke enroll**: no-hub poll **1ΓÇ»s**; retry backoff **0.5ΓÇô5ΓÇ»s**; enroll UDP
   timeout **2ΓÇ»s**; **stay watching** after first success and emit
   `spoke_enroll_reconnect` when handshake is never/stale (>12ΓÇ»s).
3. **Bank WLAN announce** every **3ΓÇ»s** (`EDGE_BANK_ANNOUNCE_S`, was 15) so
   cold spokes learn hub uplink IP inside the budget.

uartd ABI unchanged (**4.0.0.9**).

## 2026-09-10 ΓÇö EG **4.0.0.60** (`empty-harvest-x9-retry`)

Different angle after **4.0.0.59** porch still missed pos4 x1 (`post_de=0`,
full ~129ΓÇ»ms peel, good `89` self-echo). Master same-seq `89` retries were
deferred (`local_game_txn_exclusive` / `await_x1_peel`) until MRT ΓÇö no second
chance on copper.

On `auto_close_miss` with **good self-echo** (delivery proved, harvest empty):

1. Arm `_empty_harvest_x9_retry_ok` so same-seq local x9 is **not** peel-deferred.
2. Lift any deferred Master x9 for that round onto the inject front, **or**
3. Queue one `empty_harvest_x9_reinject` of the missed x9.

uartd ABI unchanged (**4.0.0.9**).

## 2026-09-10 ΓÇö EG **4.0.0.59** (`pihs-x1-capture`)

Tighten post-inject **local x1 capture** under Pi HS (residual MRT class after
4.0.0.58): good `89` self-echo + glass MRT (T340 sent `81`) but `post_de_len=0`
/ empty HS window.

1. **DE release** (`pog422_inject.c`): stop 1 ms `WAIT_TX_EMPTY` pad-loops that
   overshot air+margin (`de_hold` ~12 ms on misses while HS already live). When
   Pi HS + peer HS active past air time, free DE immediately.
2. **Peel porch** (`pog422_uartd.c`): imm first-byte gate == hard wall **60 ms**
   (was quiet-exit 20 / hard 45); post_de **45 ms**; HS x1 wait **60 ms**;
   auto_close pending **150 ms**; C FIFO grace **45 ms**. Re-assert Pi DE=0 at
   imm peel start.
3. Python local post-x9 FIFO grace **45 ms** (was 25).

uartd ABI **4.0.0.9** ΓÇö rebuild inject-shim on each Pi.

## 2026-09-10 ΓÇö EG **4.0.0.58**

Fix false mid-spin `mrt_risk_hand_txn_restart` (slots exposed hardest under Pi HS):

Once local x1 is known for this play_sequence (`await_0d` / `x1_pending`), a
duplicate same-seq local x0 must **not** clear await or bump `hand_gen`. Overnight
soak showed early inject peel arming close, then a second `80`/`40` PC ~1 ms later
orphaning the seal ΓåÆ `stale_hand_gen` / glass MRT. T340 will not re-wager until
synth `0D`; that second PC is not a new game. Same-seq re-Play while still in
`await_x1` (true miss recovery) still restarts.

uartd ABI unchanged (**4.0.0.8**).

## 2026-09-09 ΓÇö EG **4.0.0.57**

Wlan-star soak follow-ups after 4.0.0.56 AutoPlay/MRT:

1. **ETH_PORTS gretap-only** for hub/spoke ΓÇö never put `wlan*` on the AF_PACKET
   game plane (`run_bridge_edge_bench.sh` / `run_bridge_edge_prod.sh` /
   `pog_edge_env.sh`). `atlas_wifi_star` renders `EDGE_GAME_IF=none`. Link watch
   may still use uplink WiΓÇæFi separately. Stops `eth_rx ??` / `bad_frame` junk
   from POGCOM RF being treated as game frames.
2. **MRT Pi HS `89`/`81` porch** ΓÇö after early `peer_x1` harvest already queued
   `await_0d`, do not re-call `_mrt_risk_arm_after_local_x9` (false
   `mrt_risk_arm_skip` `await_0d_active`). On good closing 0D, clear
   `_local_hand_x9_gen` so the next local x0 is a cold hand (no false
   `mrt_risk_hand_txn_restart`).

uartd ABI unchanged (**4.0.0.8**).

## 2026-09-09 ΓÇö EG **4.0.0.56**

Pos3 unhappy after 4.0.0.55 Pi HS tip:

1. **BCM8/9 were never configured as inputs** (`pinctrl get` showed `no | --`)
   so uartd Pi HS sense was blind ΓÇö `packets.raw` empty, no `hs_active` /
   `packet_complete`. Fix: `pog422_pi_hs_ensure_pins()` sets IN `ip pu` and
   OUT `op dl` for machine+OIS.
2. **ethΓåÆUART thread crash** `select` on fd `-1` when hub `reopen_port` races
   enroll restore ΓÇö filter invalid socks + catch ValueError.

uartd ABI **4.0.0.8**.

## 2026-09-09 ΓÇö EG **4.0.0.55**

Bridge/uartd **Pi HS inject** for `POG422_HS_PATH=pi` (keep J3/J4 on PI ΓÇö do not
fall back to UART GPIO7 / 3 ms USB HS). When enabled:

- **machine:** DE **BCM11**, IN **BCM9** (active-low)
- **OIS:** DE **BCM10**, IN **BCM8** (active-low)

`pog422_inject_burst` uses `EXTERNAL_DE` (no GPIO7) + pinctrl DE; `slv_in_live` /
capture gating sense Pi IN. uartd ABI **4.0.0.7**. Rebuild `inject-shim` on each
Pi (`make` in `inject-shim/`).

## 2026-09-09 ΓÇö EG **4.0.0.54**

**Spoke enroll writes wg0.conf via sudo + live hub Peer.** After hub accept,
spokes failed `Permission denied: /etc/wireguard/wg0.conf` (root:700 dir) so
no Endpoint / handshake while hub already had peers. `_write_wg_conf` uses
`sudo install`; then `wg set` peer+endpoint live. uartd ABI still **4.0.0.6**.
XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.53**

**Hub `wg set` / gretap via ``sudo -n`` when bridge is topbox.** Enroll reached
hub but `apply_failed` ΓÇö Operation not permitted (bridge drops root after
tunnel_up). Pos3 (`.187`) was still on **4.0.0.48** with root:700 spoke keys
ΓåÆ Permission denied on `wg_private.key` (same class as earlier pos2/4).
uartd ABI still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.52**

**WG enroll is ad hoc anytime (no timed window).** Hub accepts new spokes
whenever fleet HMAC + bank match; gates are crypto/bank, not
`EDGE_OPEN_ENROLL` countdown. Optional lockdown: `EDGE_ENROLL_LOCK=1` restores
allowlist/window behavior. Also export `EDGE_OPEN_*` / hub IP keys from
`run_ethergold.sh` (were sourced but not exported). uartd ABI still **4.0.0.6**.
XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.51**

**Spoke enroll uses `EDGE_BANK` (not empty GAMESTATS).** Bench seats with
blank DyzineFrame `config.ini` had `bank_number=0`; enroll sent bank 0 while
hub had `EDGE_BANK=1` ΓåÆ `enroll_denied` / `bank_mismatch` / no WG peers.
Prefer Apply `EDGE_BANK` then identity. uartd ABI still **4.0.0.6**.
XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.50**

**Spoke enroll no longer dies on root:700 `/etc/ethergold/spoke`.** Bookworm
`Path.is_file()` re-raises EACCES ΓÇö wrap and fall through to writable keys.
Apply `wlan_star_apply` chowns spoke key dir to `topbox` so HTTP-as-root
Apply does not strand enroll. Ops residual: Jul-25 root keys on Duluth
spokes were chowned live. uartd ABI still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.49**

**Hub bank-wlan discovery no longer waits for meters.** Hub publishes
`discovery` at start and periodic `keepalive` every ~15s even with no
`0x0A`, so late spokes learn `hub_uplink_ip` / enroll without typing Hub
uplink IP. Prior: `maybe_keepalive` returned until the first meter ΓÇö spokes
stuck on `spoke_enroll_wait` / `no_hub_ip`. uartd ABI still **4.0.0.6**.
XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.48**

**/config Apply owns wlan-star WG (Pi SoT).** Writes `EDGE_SPOKE_ID` from
Position Number (pos1 spokeΓåÆ101), rewrites local `wg0.conf` on Apply, enroll
returns `hub_wg_pub` and installs spoke Peer. Master=Hub is Node Role (any
seat). PC `provision_wlan_star.ps1` demoted. uartd ABI still **4.0.0.6**.
XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.47**

**/config Live tracks the running HTTP tree.** `live_version` is the
WorkingDirectory stamp (`/dev` or `/prod`), not only OTA `prod/`. Shows
`tree /dev|/prod` and `outside Promote (OTA ΓÇª)` when tip/SCP is ahead of
the channel package. Staging/Rollback math still uses OTA package.
uartd ABI still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.46**

**/config Node role was not actually read-only.** CSS
`label:has(+ :read-only)` used the `:read-only` pseudo-class, which also
matches `div` / sibling `label` / radios ΓÇö so ΓÇ£Node roleΓÇ¥ and ΓÇ£Master HubΓÇ¥
got a false READ-ONLY badge. Scope to `input[readonly]` /
`textarea[readonly]` (P3 GAMESTATS fields only). uartd ABI still
**4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.45**

**/config keeps Node Role selected after reload.** `fillForm` rebuilt
role radios via `syncUi()` and cleared the checked hub/spoke radio.
Preserve via `_pendingNodeRole`. Also recover `node_role` from applied
`edge.env` (`EDGE_TOPOLOGY` / `EDGE_ROLE`) when JSON is blank. uartd ABI
still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.44**

**HTTP tip alignment + banner tracks VERSION.txt.**
1. `floor_games_cache.HTTP_BUILD` was a hardcoded leftover (`4.0.0.39`) so
   `/health` kept showing .39 after tip **4.0.0.43**. Now reads tip
   `VERSION.txt` (no separate HTTP product stamp).
2. `update.sh` reinstalls `ethergold-http` from tip when the unit
   `WorkingDirectory` is still `~/EtherGold/prod` (stale
   `field_config_ui.html` ΓÇö readonly TID). Fixture pos4 hit this.
uartd ABI still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.43**

**POG422 TID is editable EtherGold identity ΓÇö not P3-locked.** Hub/spoke/
listen type 4-hex TID on `/config`. P3 GAMESTATS panel is optional (DF/OHD);
auto-grab on login only for listen profiles. Hub stamps `FIELD_TID` when set.
19CSTS / non-Atlas cabinets do not need a P3 for EtherGold. uartd ABI still
**4.0.0.6**. XR **2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.42**

**Always park BCM10/11 output-low.** Identity Apply and listen/bridge
startup drive OIS_OUT / M-S_OUT `op dl` even when `POG422_HS_PATH=uart`.
UART protocol still uses XR GPIO1 for HS sense/drive ΓÇö Pi pins are not
toggled for handshake. Prevents TXS pull-up / DE-stuck when jumpers are
on PI but config still says UART. uartd ABI still **4.0.0.6**. XR
**2.2.2.11**.

## 2026-09-09 ΓÇö EG **4.0.0.41**

**Dev capture rotate + rclone offload.** Bridge main loops
(`uart_to_eth` / dual) call `try_rotate_debug_logs` + `idle_flush` on the
stats cadence when HS is idle (listen already did). New
`sync/offload_rotated_capture.sh` rclone-copies only stamped siblings
(`events.jsonl.*`, `packets.raw.*`, ΓÇª) to `DROPBOX_DEST/data/` then deletes
local; live basenames stay open. Optional user timer
`install_offload_rotated_capture_schedule.sh` (~10 min).
`stop_fixture_session.sh` pre-offloads before live sync. Default rotate still
**25ΓÇ»MiB** (`FIXTURE_DEBUG_LOG_MAX_BYTES`). uartd ABI still **4.0.0.6**. XR
**2.2.2.11**.

## 2026-09-05 ΓÇö EG **4.0.0.40**

**FIXTURE_POS is routing only, never a run gate.** Field Apply already
omits it from `ethergold.env`; collector systemd no longer requires
`--fixture-pos` on ExecStart (empty env previously crash-looped
`argument --fixture-pos: expected one argument`). Listen/bridge
launchers soft-default to 1 with WARN. `resolve_fixture_pos()` + optional
`--fixture-pos` on common CLI. Lab synth seat checks unchanged. uartd ABI
still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-05 ΓÇö EG **4.0.0.39**

**Listen-only OHD Mini profile.** New `listen_ohd_mini` (1920├ù360 / 24"
panel): stamps DF `display_mode=MINI`, `test_mode_no_serial=True`,
`disable_ads=True`, empty `progressives_source`. Operator chooses active
games via `floor_games_source`=`local_p3`|`http` + `floor_games_http_host`.
Grab GAMESTATS also refreshes `~/EtherGold/floor-games.json`. uartd ABI
still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-03 ΓÇö EG **4.0.0.38**

**Listen role never stamps inject=1.** `set_ethergold_role.sh` defaulted
`POG422_UARTD_INJECT=1` before the role case, so `ETHERGOLD_ROLE=listen`
commission wrote inject=1 into `/etc/ethergold/ethergold.env` (collector
setdefault could not override an existing key). Listen now defaults and
hard-forces inject=0. uartd ABI still **4.0.0.6**. XR **2.2.2.11**.

## 2026-09-02 ΓÇö EG **4.0.0.37**

**Status CREDITS follows live Heltec telemetry.** `/EtherGold/status` read
`machine-status.json` `pogcc`, which listen only refreshes on daisy packets.
Listen down left pennies frozen (e.g. $0.55) after GAMESTATS had already
zeroed Heltec. Runtime overlays `pogcc-telemetry.json`. Telemetry write
force-publishes machine-status. uartd ABI still **4.0.0.6**. XR **2.2.2.11**.
Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-09-02 ΓÇö EG **4.0.0.36**

**pogcc-com log is a capture file.** Heltec `/heartbeat` `/credits` and P3
GAMESTATS used to print only to journalctl, so `sync_to_dropbox.sh` never
showed why Heltec bailed at 4 s. Now `~/EtherGold/dev/logs/pogcc-com.log`
(line-buffered, rotate 1 MiB ├ù 2). Logs `P3 open begin` *before* pyserial
open, elapsed ms, `cmd=heartbeat done`, rx-queue drops. CLEAR_CAPTURE always
keeps `pogcc-com.log*`. uartd ABI still **4.0.0.6**. XR **2.2.2.11**. Does
**not** set `POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.35**

**Collector stop must release BCM9.** `ETHERGOLD_UARTD_PERSIST=1` plus
`KillMode=process` and `start_new_session` used to leave `pog422_listen`
running after `systemctl stop ethergold-collector`. Persist is uartd
only; supervisor SIGTERM now always `stop_listen()`. `start_listen_background.sh`
prints the leftover-child pkill when collector is already inactive.
uartd ABI still **4.0.0.6**. XR **2.2.2.11**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.34**

**GPIO9 EBUSY is another listen, not SPI.** `hat_listen.sh` used to
auto-restart with no sleep on `GPIO9 already claimed`, so a second
`start_listen_background.sh` while systemd `ethergold-collector` held
BCM9 spun hundreds of FATALS/sec. Now: abort on `already claimed`;
2 s sleep on other auto-restarts; `start_listen_background.sh` pgrep
`pog422_listen.py` / `hat_listen.sh` (not only `pog422_fixture_listen`);
`stop_fixture_session.sh` same plus warns if collector `Restart=always`
will bring listen back. EBUSY text no longer says SPI0. Persist trap
prints only when UARTD is actually on. New collector installs use
systemd `$FIXTURE_POS` from EnvironmentFile (live unit still has the
install-time `--fixture-pos` until reinstall). uartd ABI still
**4.0.0.6**. XR **2.2.2.11**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.33**

**Listen flags live in the env file, not the command line.**
`start_listen_background.sh` now sources `source_ethergold_env.sh` the same
way `hat_listen.sh` / `run_listen_jun11_good.sh` already did, so workshop
listen is `cd ~/EtherGold/dev && bash start_listen_background.sh`. Keys:
`~/EtherGold/fixture-bridge.env` and `/etc/ethergold/ethergold.env`.
Hostname `topbox6` maps to `FIXTURE_POS=6`. Launchers still force
`POG422_UARTD_INJECT=0`. uartd ABI still **4.0.0.6**. XR **2.2.2.11**.
Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.32**

**GPIO-fall short frame: hold until known wire length.** Native listen
`sessions/2026-09-01_1742-UTC_pos6-native-t340-listen-400030/` missed 2/462
`0x49` because USB delivered 18+26 and GPIO fall finalized the head
(`split_empty`). Copper HS stayed high the whole 44 B. On fall, if the
buffer is `SOH` + a `COMMAND_WIRE_LEN` opcode and still short, do not
parse: `handshake_active` goes false (no DE/glue hold) and leftover tty
bytes append for **1 ms** or until the next GPIO rise. Completeness is
wire length, not interior `0x0D`. Recovers the 18+26 class. uartd ABI
still **4.0.0.6**. XR **2.2.2.11**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.31**

**CPU affinity is operator-disable.** `~/DyzineFrame/run/cpu_affinity.disabled`
(HDMI Supervisor **CPU pin ΓåÆ Disable**, or `set_cpu_affinity.sh disable`) turns
the 4-lane pin off so DyzineFrame uses all cores again ΓÇö 2-core exclusive
starved MP4; 0ΓÇô2 still hurts fireworks. `cpu_layout.sh` / `install_cpu_affinity.sh`
honor that flag (including under `sudo`, path `/home/topbox/...`) so `update.sh`
cannot silently re-pin. Disable live-`taskset`s DF + EG pids onto all CPUs and
removes systemd `CPUAffinity` drop-ins. `OHD_CPU_FORCE=1` ignores the flag.
uartd ABI still **4.0.0.6**. XR **2.2.2.11**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.30**

**Listen freeze: do not hold `handshake_active` on capture I/O.** 4.0.0.29 soak
glued 170ΓÇô214 B leftover UART and blocked retries / x1 for hundreds of ms
because `emit` rotated, `packets.raw` / `inject_echo.jsonl` flushed, and
hs_witness printed on every packet while GPIO was not sampled. Then the 500 ms
gate was already open, so the next `0x40` landed ~10 ms after x1 and stomped
Master `0x0D` ACK. Now: rotate/flush only when HS idle (`try_rotate` +
`idle_flush`; `inject_echo.jsonl` rotates with events/packets); packet hooks
run before disk; leftover UART does not set `handshake_active`; soak drains
the 9th-bit tee without jsonl-emitting listen `wide_rx`; **25 ms close porch**
after x1 before the next wager. XR **2.2.2.11**. uartd ABI still **4.0.0.6**.
Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-09-01 ΓÇö EG **4.0.0.29**

**Idle UART is USB-late copper, not ΓÇ£bytes while DE inactive.ΓÇ¥** Master cannot
clock bits after he releases DE. If BCM9 already fell and XR delivers `05,49`
a millisecond later, those bytes belonged to the window that just closed.
Handshake idle drain now parses them (`uart_after_hs_fall` ΓåÆ
`packet_complete`) instead of `bytes_discarded_inactive`. Overnight seq **125**
was this class: discarded Master ACK, then three extra `0x40` retries.
Does **not** wait on USB at GPIO fall (that was the 4.0.0.28 2 s jam).
XR **2.2.2.11**. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`. `inject_echo.jsonl` rotate is still later.

## 2026-09-01 ΓÇö EG **4.0.0.28**

**Empty-window HS finalize and own-echo wait must not USB-block.** Overnight
pos6 soak (EG 4.0.0.27) proved BCM9 sees our BCM11 DE: every `0x40` has a
2ΓÇô3 ms empty HS window, and `_handshake_finalize_window` still called
`uart_rx_snapshot` (GET_REG / GET_RX_STATS). Four of five timeouts were us
jamming the daisy (~2 s DE or discarded Master `0x49`). Handshake now drains
via `uart_drain_no_usb` (`os.read` nonblock) and skips USB snapshot on empty
windows (`verdict=skipped_usb_snapshot`). SuperBall `pulse_de_write` waits
echo with `wait_own_echo_from_tty` so DE falls by the ~80 ms cap even if XR
`in_waiting` would stall. Overnight proof: seq 89 discarded 214 B Master ACK;
seq 105/228/212 held DE 2.1ΓÇô2.4 s. XR **2.2.2.11**. uartd ABI still **4.0.0.6**.
Does **not** set `POG422_UARTD_INJECT=1`. `inject_echo.jsonl` rotate is still
later (after Chris deletes the 4.0.0.27 landing).

## 2026-08-31 ΓÇö EG **4.0.0.27**

**Wide RX 9th-bit log + same-seq 0x40 retry 125 ms ├ù3.** UART already
receives 9-bit (preciseflags + CS5); soak `ser.read` is 8-bit stripped.
Player now enables the kernel wide RX tee (`SET_WIDE_RX`) and logs
per-byte MARK/SPACE (`echo_9th` / `ninth_flags` on `inject_echo`, plus
`wide_rx` events for listen). Missing Master `0x49`: re-TX the same
seq `0x40` at 125 ms, three times, then `x9_timeout` (2 s cap if HS
stays busy). Retries do not increment plays or move the 500 ms gate.
XR **2.2.2.11**. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.26**

**Dev soak logs sent vs echo hex.** SuperBall player compared echo to sent in
RAM (`echo_ok`) but archives only kept verdict / length / last byte, so EVAL
could not forensically diff a miss. `pulse_de_write` now calls
`write_inject_echo` with `full_hex=True`: `sent_hex` / `echo_hex` / `match` /
`match_kind` on `events.jsonl` `inject_echo`, plus `inject_echo.jsonl`. Prod
bridge emit stays prefix-only. XR **2.2.2.11**. uartd ABI still **4.0.0.6**.
Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.25**

**Pi-HS soak holds BCM11 until own echo ends `0x0D`.** Skip-FIFO soak
(4.0.0.24 / XR 2.2.2.11) dropped DE when `INJECT_TX` returned (~8.1 ms,
`drain_wait` 7.80). Echo was 22/23 cropped (`F9`/`FF`/`F1`); Master LNT
was HS blip only (23 TX / 0 TID **1002** `0x49`). Native `0x40` HS is
~11 ms. Player now keeps EXTERNAL_DE / skip FIFO / gpio7=0, waits
`wait_own_echo` **before** `de.fall()`, then drops. Echo timeout (~57 ms)
is still the jam cap. Log `de_hold=own_echo_eop`. Do **not** restore 18 ms
49ΓåÆ41 think-time. Do **not** set `POG422_UARTD_INJECT=1`. XR stays
**2.2.2.11**. uartd ABI still **4.0.0.6**.

## 2026-08-31 ΓÇö EG **4.0.0.24**

**`update.sh` re-execs after pull** so the process uses the script that
just landed. 4.0.0.23 Python pulled, then the still-running 4.0.0.22
script `FATAL`d `uartd 4.0.0.6 != tree tip 4.0.0.23` (the words
`tree tip` are only in the old script). Same-turn unblock is
`SKIP_PULL=1 bash update.sh` on the copy already on disk. uartd ABI
still **4.0.0.6**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.23**

**`update.sh` uartd check uses `UARTD_ABI_VERSION`, not `EG_VERSION`.** 4.0.0.22
pull on `.140` rebuilt uartd (4.0.0.6 in the ELF, correct) then
`FATAL: after make, uartd version 4.0.0.6 != tree tip 4.0.0.22` and
never reached XR **2.2.2.11**. Changelog already said ABI-not-tip;
the inject compare still used tree tip (old pos5 ΓÇ£must match PythonΓÇ¥
guard). Driver MODULE_VERSION compare from 4.0.0.22 stays. uartd ABI
still **4.0.0.6**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.22**

**`update.sh` installs XR when tree `MODULE_VERSION` Γëá loaded.** 4.0.0.21
pull on `.140` left driver **2.2.2.10**: `_driver_needs_install` treated
in-range `[EG_COMPONENT_MIN, EG_VERSION]` as done, and 2.2.2.10 still
falls in `[2.2.2.10, 4.0.0.21]`. Soak `EXTERNAL_DE` needs **2.2.2.11**.
Now install when `xr_usb_serial_common.c` `MODULE_VERSION` Γëá sysfs.
Python-only stamps still skip driver (MODULE_VERSION unchanged).
`EG_COMPONENT_MIN` stays **2.2.2.10** for listen. uartd ABI still
**4.0.0.6**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.21**

**Pi-HS soak DE closer to native air (skip FIFO get_reg).** Native T340+
`0x40` / `0x41` HS is ~11 / ~5 ms. Soak was ~35 / ~30 ms because BCM11
rose before `INJECT_TX` and the ioctl ran two TX_FIFO `get_reg` USB
controls with DE already high. XR **2.2.2.11** `POG422_INJECT_POST_EXTERNAL_DE`
(`0xFFFFFFFD`) skips those controls and does **not** assert GPIO7
(`MANUAL_DE` / `0xFFFFFFFE` still refused). Soak refuses to TX on XR
older than 2.2.2.11 ΓÇö that magic is a drain margin on 2.2.2.10.
Listen still accepts driver **2.2.2.10** (`EG_COMPONENT_MIN` unchanged).
`0x41` still waits HS inactive after Master `0x49`, then fires (do not
add the native 18 ms think-time ΓÇö progressive-win test is x1 on the
wire before Master resends x9). Local `0x0D` synth stays later (Eth /
WiFi RTT). uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.20**

**Pi HS listen: kernel GPIO edges + no USB snapshot in the hot path.** Chris:
slave `0x41` must drop HS before Master `0x0D` ΓÇö the four 4.0.0.19
`window_glued` 41+0D rows were the same listen miss as 49+41, just a
shorter dip (~1 ms extra in `byte_span` vs continuous air). Polling BCM9
once per tick still hid it because `uart_rx_snapshot` (FIONREAD + XR
GET_REG) ran **every** open window. Now `GpioHandshakeMonitor` requests
rise/fall events (1024-deep kernel buffer, non-blocking drain) and
`handshake_capture_tick` splits windows on those edges. USB snapshot
stays at window open/close only. uartd ABI still **4.0.0.6**. Does
**not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.19**

**Listen must not block 20 ms on UART without sampling HS.** Pi HS listen
(`handshake_capture_tick`) used `select()` with
`HS_UART_INTER_CHAR_POLL_S = 0.02` after sampling BCM9 once. During that wait
Master drops HS, the T340+ takes the bus, and `0x41` bytes land in the same
buffer ΓÇö `window_glued` / one `t_hs_rise` on both packets. Duluth SuperBall
looked correct (155/155 separate, ~21 ms) because the x9ΓåÆx1 gap was just
longer than 20 ms so `select` timed out and GPIO ran. Native T340+ pos6
listen 17:41 AST glued **73/101**. Window still ends on GPIO inactive, not
UART idle: non-blocking `in_waiting` drain, re-sample HS before the read.
Constant removed. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.18**

**P3 screen capture on field Home.** New Home tile **P3 capture**
(`/EtherGold/p3-capture`). HTTP writes `~/DyzineFrame/run/p3_grab.request`;
DyzineFrame **4.0.1.10** runs TARGAFILE on the P3 port it already owns
(same as S key) and writes PNG + TGA under `~/p3-grabs`. Browser fetches
the PNG with the UI session and **Save PNG on this device** (Downloads).
HTTP never opens P3. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.17**

**Sample HS immediately before SuperBall slave DE rise.** Pos6 11:09 soak
(**4.0.0.16**, DE on ioctl return) was **111 / 109 / 109** with two
`x9_timeout`s (seq 68, 102). Echo **220/220** last `0x0D` ΓÇö not a crop.
Those two misses had HS idle **368ΓÇô391 ms** before DE; the remaining
race is successful rounds that snatch the bus **0.92 ms** after
`hs_inactive`. Loop already gated `hs_busy`, then built the frame, then
`de.rise()`. Now `pulse_de_write` samples BCM9 twice (200 ┬╡s apart)
immediately before asserting DE and aborts (`hs_busy_pre_de`) if another
machine is driving. Skip does not increment seq / open a round / ACK.
GPIO7 idle. `~/EtherGold/dev` only. uartd ABI still **4.0.0.6**. Does
**not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.16**

**Soak echo must end 0x0D after INJECT_TX DE drop.** Chris: a premature DE
drop could crop the packet; validate the local echoΓÇÖs last byte is `0x0D`.
DE still falls when `INJECT_TX` returns (do not hold for echo). After DE is
low, classify echo: `echo_ok` (full frame + last `0x0D`), `echo_cropped`
(bytes but not a complete EOP frame), `echo_timeout` (empty). Crop/timeout
emit `error`. `session_stop` counts `echo_ok` / `echo_cropped` /
`echo_timeout`. GPIO7 idle. `~/EtherGold/dev` only. uartd ABI still
**4.0.0.6**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.15**

**SuperBall slave DE drops when INJECT_TX returns.** Pos6 wide soak (14:13Z)
was **207 / 201 / 201** with five `x9_timeout`s. Those five `0x40`s echoed
fine (`usb_actual=84`) but Master never sent that seqΓÇÖs `0x49` (three waits
had **no HS for 2 s**). Successful `0x40`ΓåÆ`0x49` floor was **52 ms** ΓÇö BCM11
held ~33 ms for userspace echo after the kernel had already waited ~8 ms
wire. Fast Master results start as soon as we release DE; holding through
echo collides with that path and Master does not retry. Echo is still
logged (`inject_echo`). GPIO7 stays idle. `~/EtherGold/dev` only. uartd ABI
still **4.0.0.6**. Does **not** set `POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.14**

**SuperBall slave TX is kernel INJECT_TX wide (MARK first / SPACE rest).** T340+
throws 8-bit SPACE-on-every-byte frames away; pyserial `write()` cannot switch
parity between bytes without a gap. Soak now calls `XR_USB_SERIAL_INJECT_TX`
(payload only; kernel packs `[data][0x01]` then `[data][0x00]`). BCM11 still
owns DE. `post_margin_us` is never `MANUAL_DE` (`0xFFFFFFFE`) ΓÇö that would
assert XR GPIO7. Preciseflags + GPIO MODE/DIR after `open_serial(hs_source=pi)`.
Workshop `~/EtherGold/dev` only. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-31 ΓÇö EG **4.0.0.13**

**SuperBall slave DE hold is own UART echo through 0x0D.** Last soak (pos6,
08-30 ~22:22) pulsed BCM11 ~12 ms after `flush()` (`POST_TX_MARGIN_US=400`) and
the Master saw HS tick only. Hold DE until `ttyXRUSB1` RX matches the frame we
just sent, including the terminating `0x0D` (do not stop on an interior 0x0D).
Timeout still drops DE so a missing echo cannot jam the daisy. Drive BCM11 via
gpiochip ioctl when the line is free; `pinctrl` remains the fallback. Workshop
`~/EtherGold/dev` only. uartd ABI still **4.0.0.6**. Does **not** set
`POG422_UARTD_INJECT=1`.

## 2026-08-30 ΓÇö EG **4.0.0.12**

**SuperBall slave soak stays in `/dev`.** `Promote-ToProductionStaging` / `build_prod.sh`
were copying `superball_slave_*.py` and `run_superball_slave_soak.sh` into `/prod`
(4.0.0.11). Those are workshop-only (`~/EtherGold/dev`, `ARM_TX=1`). Strip them
from the prod fileset. uartd ABI still **4.0.0.6**.

## 2026-08-30 ΓÇö EG **4.0.0.11**

**SuperBall $20 slave soak (Pi HS, bench `.140`).** New
`superball_slave_wager.py` / `superball_slave_player.py` / ledger: TID **1002**
only (never Master TID), SuperBall `930A` $20 (80 credits) picks 1ΓÇô5, pyserial
`ttyXRUSB1`, BCM9 HS IN, BCM11 DE pulse (`op dh`/`op dl`), 500 ms start-to-start
`0x40`s, `transactions.jsonl` + `jackpots.jsonl` (`progwin>0` + next `0x0A`
game-11 `reset_ok`). TX requires `--arm-tx` / `ARM_TX=1`. Capture seat
**`FIXTURE_POS=6`** / hostname **`topbox6`** ΓåÆ Dropbox `EtherGold/logs/6`
(`install_sync.sh` maps `topbox1..6`; `stamp_pos6_capture.sh`). Does **not**
convert GA 1ΓÇô5, does **not** set `POG422_UARTD_INJECT=1`. uartd ABI still
**4.0.0.6**.

## 2026-08-30 ΓÇö EG **4.0.0.10**

**Listen crash on first gameplay packet.** `observe_listen_current_game`
called `map_wire_game_to_index` without importing it from
`current_game_ipc` ΓåÆ `NameError` after the first `0x40`/`0x49` window,
killing HS-gated listen mid SuperBall AutoPlay. Import fixed. uartd ABI
still **4.0.0.6**.

## 2026-08-30 ΓÇö EG **4.0.0.9**

**Listen x0 checks DF HDMI game.** After each gameplay request (x0 wager),
listen reads `df-displayed-game.json`. If missing, pre-boot, logo (`0`), or
a different 1..16 than copper, it force-rewrites `current-game.json` even
when the file already has SuperBall. DF **4.0.1.9** applies that rewrite.
Does not rewrite on every x0 when HDMI already matches. uartd ABI still
**4.0.0.6**.

## 2026-08-30 ΓÇö EG **4.0.0.8**

**GET /health lists EtherGold services.** Aggregate `version` stays `EG_VERSION`.
Payload adds `product=ethergold` and `services` (http, listen, collector,
pogcc_com). Existing `listen` block unchanged. Health HTML shows a Services
row. uartd ABI still **4.0.0.6**.

## 2026-08-30 ΓÇö EG **4.0.0.7**

**EtherGold is one product.** HTTP listen/pogcc-com work from this morning
(was letter **30-h**: keep Heltec ACM open / DTR; listen TID not spoke-only)
now ships as **4.0.0.7**. `/health` `version` is `EG_VERSION`. OTA Staging
shows **4.0.0.7**. uartd/inject ABI stays **4.0.0.6** (no UART rebuild).
Does **not** start pogcc-com unless `pogcc_com_owner=ethergold`.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-h` (folded into EG **4.0.0.7**)

**Do not close Heltec ACM.** 30-g hang watchdog closed the CDC fd at 20s
(`pump hung ΓÇö close CDC fd`). That drops DTR; Heltec reports the port
closed even though `303a:0002` / `ttyACM1` stay on the bus. Closing
`/dev/pts/1` every loop also stalled the pump. No hang abort, no 15s
close/reopen, no pts `os.close`. Hold ACM open for DTR. Does **not** bump
`EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-g` (still EG **4.0.0.6**)

**Force `O_NONBLOCK` with `fcntl`; ACM `os.read` on a side thread.** 30-f
still blocked in `os.read` (wchan `do_select`) before any hello; hang
watchdog closed the fd from another thread and cdc_acm did not wake.
Heltec theme applied then `heartbeat() Blocking took 4000ms`. Main loop
only `Queue.get_nowait()` so status/`HB,` keep running if ACM read
stalls. `F_SETFL` after `os.open`; hang abort 20s; drop leftover
`/dev/pts` every loop. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-f` (still EG **4.0.0.6**)

**Do not `select()` on Heltec ACM.** 30-e opened ACM then the hang
watchdog fired in 3s (`pselect` on this gadget ignores the timeout).
CDC I/O is non-blocking `os.read`/`os.write` + `BlockingIOError`;
Python `time.sleep(0.01)` on EAGAIN. Loop always ticks so `HB,` can
be answered after theme. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-e` (still EG **4.0.0.6**)

**Heltec CDC is raw `os.open` / `select`, not pyserial.** After 30-d
answered `HB,` and pushed Identity, ACM `ts` froze again. Hang watchdog
logged `pump hung ΓÇö cancel CDC I/O` but the process stayed in
`pselect6(..., writefds=/dev/pts/1, timeout=NULL)` ΓÇö not ACM. pyserial
`cancel_read` cannot unblock a pts write. Heltec then applied theme and
bailed on later heartbeats. Open ACM with `O_NOCTTY|O_NONBLOCK`, drop
inherited pts fds, unit `StandardError=journal` (was inherit). P3
GAMESTATS still pyserial. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-d` (still EG **4.0.0.6**)

**pogcc-com hang watchdog is off-thread.** After 30-c answered the first
`HB,` and pushed Identity, ACM `select` wedged (Heltec busy applying
theme; gadget IN NAKs). Status `ts` froze; same-thread 15s watchdog never
ran; Heltec then `heartbeat() Blocking took 4000ms` on every later poll.
A daemon thread `cancel_read`/`cancel_write` + close if the pump loop
stalls >3.5s. Identity is deferred until after `HB,` and written only
when asset/theme/volume changed. Poll with `select` 50ms, not blocking
`readline` on an empty ACM. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-c` (still EG **4.0.0.6**)

**pogcc-com must answer `HB,` within 4s.** Two hangs: (1) 15s CDC watchdog
called `sudo usbreset 303a:0002` (no `-n`) and blocked on a password prompt;
same landmine as DF ΓÇö self-powered Heltec, cable VBUS open. Watchdog is
close/reopen only; JTAG `303a:1001` uses `sudo -n`. (2) Identity `/asset`
`/theme` `/volume` on **hello** filled TinyUSB RX before HeltecΓÇÖs force
heartbeat readline, and pyserial `write_timeout` default None wedged the
pump so `HB,` never went out. Hello no longer downlinks; heartbeat writes
`HB,` then identity. Serial `write_timeout=0.5`. Heltec **2802** IDE
console: hello then `heartbeat() Blocking took 4000ms`. Does **not** bump
`EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-b` (still EG **4.0.0.6**)

**pogcc-com USB-pushes Identity.** When `pogcc_com_owner=ethergold`, DyzineFrame
does not hold Heltec CDC. Apply already stamped `config.ini` `pog_cc_theme` /
`pog_cc_volume` / asset, but `pogcc_com_service.py` (build `2026-08-12-c`) never
sent `/asset` `/theme` `/volume`. It does now on hello and each heartbeat, so
attract follows `/config` even if DF is down. Heltec **2801+** applies only if
the value changed (NVS). Does **not** bump `EG_VERSION` / uartd.

## 2026-08-30 ΓÇö HTTP `ethergold-http-2026-08-30-a` (still EG **4.0.0.6**)

**Listen TID is not spoke-only.** `/config` Validate used to reject any `tid` on
`listen_ohd` (`tid is only used for atlas_wifi_star slave_spoke`). That blocked
Apply when leftover `FIELD_TID` sat in the hidden spoke field, even though
listen current-game filter is the GAMESTATS 4-hex (`POG_TID`, Lucky Street
**BD07**). Listen now accepts optional 4-hex TID; placeholders/`---`/junk are
ignored. UI posts GAMESTATS `term_number` as tid on listen (not the hidden
spoke box). Hub / wifi_bank_bridge leftover is ignored. Spoke still requires
4 hex. Apply stamps `POG_TID` when listen tid is 4 hex. Does **not** bump
`EG_VERSION` / uartd. Not field-published.

## 2026-08-29 ΓÇö HTTP `ethergold-http-2026-08-29-c` (still EG **4.0.0.6**)

**Pi HS listen (workshop).** With J3/J4 on PI, XR GPIO1 is disconnected from
THVD HS. Listen keeps XR UART RX (`ttyXRUSB`) and senses handshake on BCM9
`M-S_IN` / BCM8 `OIS_IN` (`pog422_gpio_hs.py`). Forces pyserial
(`POG422_UARTD=0`) because uartd still samples XR GPIO1. Idle OUTs stay driven
low. Does **not** inject. Does **not** bump `EG_VERSION` / uartd. Not field.

## 2026-08-29 ΓÇö HTTP `ethergold-http-2026-08-29-b` (still EG **4.0.0.6**)

**Pi HS OUTs idle-low on every Pi at boot.** BCM10 `OIS_OUT` and BCM11 `M-S_OUT`
are driven low from firmware `config.txt` (`gpio=10=op,dl` / `gpio=11=op,dl`)
and systemd `ethergold-hs-out-idle.service` (sysinit pinctrl). Older
POG-COM-PI-HAT boards have those pads unconnected (safe). Rev E+ must never
leave the Pi pin high-Z (TXS pull-up holds DE on). Installer
`sync/install_hs_out_idle.sh` also runs pinctrl immediately (no reboot).
Hooked from Promote, listen-stock, and DF harden. Independent of the
`/config` UART vs Pi HS toggle. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-29 ΓÇö HTTP `ethergold-http-2026-08-29-a` (still EG **4.0.0.6**)

**Deployment profile High Speed path** is its own toggle, not a new profile.
`uart` (default) = HAT XR HS (J3/J4 UART). `pi` = Pi GPIO J19 (J3/J4 PI).
Apply writes `POG422_HS_PATH` and, on a Pi with `pinctrl`, idle-drives BCM10/11
**low** for Pi HS (never high-Z). Does **not** change `POG422_UARTD` / inject /
`FIXTURE_POS`. GPIO listen backend is not field yet. Does **not** bump
`EG_VERSION` / uartd.

## 2026-08-28 ΓÇö HTTP `ethergold-http-2026-08-28-c` (still EG **4.0.0.6**)

**Pi Identity owns Heltec theme and volume.** `/config` Identity card has attract
theme (1ΓÇô5 Slots/Numbers/Rainbow/Fire/Quiet) and DS1881 volume (0ΓÇô62). Apply
stamps `config.ini` `pog_cc_theme` / `pog_cc_volume`. DyzineFrame **4.0.1.4**
USB-pushes those plus `/asset` down; leftover Heltec `/asset` `/ip` are ignored.
Heltec Setup (`POG_CC_2026082801`) is Wi-Fi only. Does **not** bump `EG_VERSION` /
uartd. Not published unless asked.

## 2026-08-28 ΓÇö HTTP `ethergold-http-2026-08-28-b` (still EG **4.0.0.6**)

**Onsite enroll during POG-CC + Pi provisioning.** Identity Apply always
upserts this cabinetΓÇÖs Enrollment.csv row (including `listen_ohd_server`).
Config/Status Location dropdown has **new site** (type Location + Path).
POG-CC USB `/asset` upserts the same row when `config.ini` already has a
Path. Ads/content sync can use Identity Path before the asset is in the
Dropbox SGVI catalog. Pi still does not publish Dropbox SGVI. Workshop
catalog row **21044** Lucky Street was added in `SGVI/Enrollment.csv`.
Does **not** bump `EG_VERSION` / uartd.

## 2026-08-28 ΓÇö HTTP `ethergold-http-2026-08-28-a` (still EG **4.0.0.6**)

**Recover stack** on Config/Status: listen/collector first, wait for
`progressives.csv`, then DyzineFrame `supervisor.cmd` restart. Lucky Street
08-28: DF 4.0.1.3 unlinked pre-boot CSV; Atlas still saw 0x0A; HDMI banner
stayed until DF restart. Never `sudo reboot`. Never kill HDMI supervisor.
Listen supervisor no longer parks 300 s after five CSV-missing bounces.
Does **not** bump `EG_VERSION` / uartd.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-f` (still EG **4.0.0.6**)

**Staging older than Live is Rollback, not pending.** LAN/workshop live ahead of
SGVI (green disc + 03:00 would have downgraded). Stage parks the older tree into
Rollback and clears `staging-pending`. Promote skips that downgrade unless
`CONFIRM_DOWNGRADE=1`. Config copy matches. Pairs with DF **4.0.1.3**. Does
**not** bump `EG_VERSION` / uartd.

## 2026-08-26 ΓÇö listen early-win (still EG **4.0.0.6**)

**Listen-only triggers early fireworks.** Field TopBox is a copper witness, not a
bridge, but it already sees complete x9 frames. Phase 1 (progwin, including
SuperBall `930A` / `0x49`) and Phase 2 keno/slots now run on `pog422_listen.py`
via `fireworks-request.json`. Inject stays off. Listen defaults the
`FIXTURE_EARLY_WIN_*` flags **on** (bridge stays opt-in). Phase 2 cash still
needs local TID (`ETHERGOLD_LOCAL_WIRE_TID` / `POG_TID` /
`ETHERGOLD_CURRENT_GAME_TID`). Does **not** bump `EG_VERSION` / uartd.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-e` (still EG **4.0.0.6**)

**Restart DyzineFrame from Config/Status.** `POST /EtherGold/api/dyzineframe/restart`
writes `~/DyzineFrame/run/supervisor.cmd` `restart` and clears `supervisor.hold`
(same file HDMI Supervisor uses). Does **not** kill `dyzineframe_supervisor.py`.
Does **not** `sudo reboot`. Does **not** bump `EG_VERSION` / uartd. Pairs with DF
**4.0.1.1**.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-d` (still EG **4.0.0.6**)

**Health copy: SDS Enter Game writes `current-game.json`.** Not copper gameplay,
not 0x0A meters. Missing until Heltec SDS 0xA2 `/sds` (1..16) this boot is
expected on attract. Does **not** bump `EG_VERSION` / uartd.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-c` (still EG **4.0.0.6**)

**Formatted health page.** `GET /health` stays machine-readable JSON (boot_id
watch, curl). HTML twin is `GET /EtherGold/health` (no login; polls the same
JSON). Home tile + Config/Dashboard nav. Status dashboard has a Health card
after login. JSON includes `"ui": "/EtherGold/health"`. Does **not** bump
`EG_VERSION` / uartd. Does **not** Promote/Publish-Sgvi.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-b` (still EG **4.0.0.6**)

**Missing `progressives.csv` is a listen fault, and listen comes back.** Collector
supervisor no longer treats leftover `hat_listen.sh` as ΓÇ£still launchingΓÇ¥ forever
(that blocked respawn after listen Python died). After 45 s grace it bounces
listen if the CSV is absent/empty (max 5, then 300 s give-up). `GET /health`
`listen.reasons` includes `progressives_csv_missing` so `listen.ok` is false.
Does **not** bump `EG_VERSION` / uartd. Does **not** Promote/Publish-Sgvi.

## 2026-08-26 ΓÇö HTTP `ethergold-http-2026-08-26-a` (still EG **4.0.0.6**)

**Lucky Street listen hardening (workshop; unpublished).** `GET /health` now
includes `listen` (collector, listen/supervisor PIDs, `ttyXRUSB*`, uartd sock,
CSV/current-game age, inject must be 0). HTTP `ok` stays true if HTTP is up.
Collector unit + drop-in: `Type=notify` `WatchdogSec=60`; supervisor sd_notify
READY/WATCHDOG (stdlib, no python-systemd). `ota_restart_ethergold_listen`
restarts `ethergold-collector` (`KillMode=process`); does not SIGKILL the
supervisor or mention `ethergold-listen.service`. Does **not** bump
`EG_VERSION` / uartd. Does **not** Promote/Publish-Sgvi.

## 2026-08-25 ΓÇö EG hourly + 03:00 timers on promote (still EG **4.0.0.6**)

**Listen-stock only installed DyzineFrame timers**, so Lucky Street `10.1.1.39`
kept EG **4.0.0.4** with no `ethergold-stage.timer` / `ethergold-promote.timer`.
`promote_staging.sh`, `stage_from_dropbox.sh`, rollback, and
`prepare_listen_stock.sh` now run `sync/install_pi_deploy_schedule.sh`
(idempotent). Update Now / Check for Updates also sets `XDG_RUNTIME_DIR` so
`systemctl --user` works from `runuser -u topbox`. Does **not** bump
`EG_VERSION` / uartd. Does **not** `sudo reboot`.

## 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 |
