Found during the adversarial review of PR #232 (#226). The problem predates that PR and affects every UCI rig that drives a fetch.
Mechanism
If a UCI rig raises an exception, or gets a Ctrl-C, inside its poll loop, control goes to the `finally` block. That block calls `disable_uci` and then `lock.release()`. The next queued lane gets the DeviceLock straight away and calls `client.reset()`.
If the C64 still holds a live firmware socket at that point, the reset poisons the DHCP lease. `GET_IPADDR` then returns 0.0.0.0 until the device is power-cycled at the wall (CLAUDE.md, "Device gotchas": lease poisoning).
- Viewer build (`HTTPS_BODY_TO_REU`): the socket stays open until the user sends 'Q'.
- Non-viewer build: it stays open until `http_recv_body` returns, which can take up to about 87 minutes against a silent CONNECTED socket (the 65,536-tick budget).
Ctrl-C is the likely trigger, since 1 MHz runs take hours.
Why a try/finally 'Q' does not fix it
The C64 does not read the keyboard during `http_recv_body`: `getin` is called only in `main_loop` and in the viewer. So a 'Q' sent from `finally` just sits in the keyboard buffer while the lock is released.
Proposed direction
On an abnormal exit, and before releasing the lock, do a bounded best-effort wait until `net_tcp_state != CONNECTED` (address from build/labels.txt, with the $A000 shadow-RAM readability gate) or until the CONNECTION CLOSED marker appears. If neither happens within the bound, print a loud warning that the device may need a power cycle before the next lane runs. Put this in a shared helper so every UCI rig gets it, not only rig_https_banner.py.
Not established
- No lease has yet been seen poisoned by this path; the mechanism is from reading the code.
- How often rigs actually exit abnormally mid-fetch has not been measured.
🤖 Generated with Claude Code
Found during the adversarial review of PR #232 (#226). The problem predates that PR and affects every UCI rig that drives a fetch.
Mechanism
If a UCI rig raises an exception, or gets a Ctrl-C, inside its poll loop, control goes to the `finally` block. That block calls `disable_uci` and then `lock.release()`. The next queued lane gets the DeviceLock straight away and calls `client.reset()`.
If the C64 still holds a live firmware socket at that point, the reset poisons the DHCP lease. `GET_IPADDR` then returns 0.0.0.0 until the device is power-cycled at the wall (CLAUDE.md, "Device gotchas": lease poisoning).
Ctrl-C is the likely trigger, since 1 MHz runs take hours.
Why a try/finally 'Q' does not fix it
The C64 does not read the keyboard during `http_recv_body`: `getin` is called only in `main_loop` and in the viewer. So a 'Q' sent from `finally` just sits in the keyboard buffer while the lock is released.
Proposed direction
On an abnormal exit, and before releasing the lock, do a bounded best-effort wait until `net_tcp_state != CONNECTED` (address from build/labels.txt, with the $A000 shadow-RAM readability gate) or until the CONNECTION CLOSED marker appears. If neither happens within the bound, print a loud warning that the device may need a power cycle before the next lane runs. Put this in a shared helper so every UCI rig gets it, not only rig_https_banner.py.
Not established
🤖 Generated with Claude Code