Claim
PLAUSIBLE ONLY — needs hardware to confirm or refute. Filing so it is not
lost, not asserting it is a live bug.
net_tcp_send has three exit paths that return C=1 without ever reaching
uci_ack, i.e. without the DATA_ACC that returns the FPGA's state machine to
idle. Per the mechanism in #144, a transaction left un-accepted makes the next
PUSH_CMD hit else error_busy <= '1' and be silently dropped — which is
$86 on the $DF1C status bit, a code CLAUDE.md records as having been seen
in the field on otherwise-passing runs.
Verified against origin/master at dc06095, src/net/uci/net.s:
| exit |
lines |
reached when |
@sb_push's bail |
:817-818 |
uci_push_wait timed out waiting for the SOCKET_WRITE response |
@sb_err_drain_to |
:832-834 |
uci_drain_resp/uci_drain_status timed out on the UCI_ERR_SEND_FAIL path |
@sb_ok_drain_to |
:852-854 |
the same two drains timed out on the success path |
All three are wall-clock-timeout paths with net_last_error already set to
UCI_ERR_WAIT_TIMEOUT ($89), so they are honest failures — the question is
only what state they leave the device in for the next command.
The recovery primitive exists and is not wired to any of them:
$ grep -n 'jsr uci_abort' src/net/uci/net.s
107: jsr uci_abort
One call site, in net_init. Nothing on an error path calls it, so the only
thing that clears the state machine after a mid-transaction bail is the next
boot.
Why this is not a #144 reopen
#144's three named defects are all closed on master and I verified each
independently before closing it (see
#144 (comment)): the
per-byte pulse is gone, all 11 uci_ack sites run
drain_resp -> drain_status -> ack in that order, and the SOCKET_WRITE path
has its own accept on both its completing exits. This is a fourth thing,
about timeout paths rather than about queue semantics, and it is defensible as
designed — a wedged FPGA may not answer an accept either, and issuing one into
an unknown state is not obviously safer than not.
What would settle it
Hardware, and it is cheap to look for because the symptom is already
instrumented:
- Supports the claim: after any run that records
$89 UCI_ERR_WAIT_TIMEOUT
on a send, the next transaction reports $86. CLAUDE.md notes $86 has
exactly one setter in command_protocol.vhd (the PUSH_CMD-while-not-idle
branch), so it always means our push was rejected — the two appearing in that
order would be the signature.
- Refutes it:
$89 on a send with no $86 following, or $86 observed on
runs with no preceding send timeout.
CLAUDE.md's note that $86 has been seen on passing runs (github.com HTTP 200,
2026-08-28) is the reason this is worth checking rather than dismissing: a
passing run that carries one recovered from a real rejected push, and this is
a candidate source for it.
If confirmed, the fix is presumably jsr uci_abort before the sec; rts on
the two drain-timeout exits (@sb_err_drain_to, @sb_ok_drain_to) and
possibly on @sb_push's. uci_abort already writes ABORT and burns a settle
delay, so it is the intended primitive; it just has one caller.
No hardware was available to this session.
Found by adversarial review while confirming #144's closure.
Claim
PLAUSIBLE ONLY — needs hardware to confirm or refute. Filing so it is not
lost, not asserting it is a live bug.
net_tcp_sendhas three exit paths that returnC=1without ever reachinguci_ack, i.e. without the DATA_ACC that returns the FPGA's state machine toidle. Per the mechanism in #144, a transaction left un-accepted makes the next
PUSH_CMDhitelse error_busy <= '1'and be silently dropped — which is$86on the$DF1Cstatus bit, a code CLAUDE.md records as having been seenin the field on otherwise-passing runs.
Verified against
origin/masteratdc06095,src/net/uci/net.s:@sb_push's bail:817-818uci_push_waittimed out waiting for the SOCKET_WRITE response@sb_err_drain_to:832-834uci_drain_resp/uci_drain_statustimed out on theUCI_ERR_SEND_FAILpath@sb_ok_drain_to:852-854All three are wall-clock-timeout paths with
net_last_erroralready set toUCI_ERR_WAIT_TIMEOUT($89), so they are honest failures — the question isonly what state they leave the device in for the next command.
The recovery primitive exists and is not wired to any of them:
One call site, in
net_init. Nothing on an error path calls it, so the onlything that clears the state machine after a mid-transaction bail is the next
boot.
Why this is not a #144 reopen
#144's three named defects are all closed on master and I verified each
independently before closing it (see
#144 (comment)): the
per-byte pulse is gone, all 11
uci_acksites rundrain_resp -> drain_status -> ackin that order, and the SOCKET_WRITE pathhas its own accept on both its completing exits. This is a fourth thing,
about timeout paths rather than about queue semantics, and it is defensible as
designed — a wedged FPGA may not answer an accept either, and issuing one into
an unknown state is not obviously safer than not.
What would settle it
Hardware, and it is cheap to look for because the symptom is already
instrumented:
$89 UCI_ERR_WAIT_TIMEOUTon a send, the next transaction reports
$86. CLAUDE.md notes$86hasexactly one setter in
command_protocol.vhd(thePUSH_CMD-while-not-idlebranch), so it always means our push was rejected — the two appearing in that
order would be the signature.
$89on a send with no$86following, or$86observed onruns with no preceding send timeout.
CLAUDE.md's note that
$86has been seen on passing runs (github.com HTTP 200,2026-08-28) is the reason this is worth checking rather than dismissing: a
passing run that carries one recovered from a real rejected push, and this is
a candidate source for it.
If confirmed, the fix is presumably
jsr uci_abortbefore thesec; rtsonthe two drain-timeout exits (
@sb_err_drain_to,@sb_ok_drain_to) andpossibly on
@sb_push's.uci_abortalready writes ABORT and burns a settledelay, so it is the intended primitive; it just has one caller.
No hardware was available to this session.
Found by adversarial review while confirming #144's closure.