Type: Bug
Priority: Low (intermittent; observed once)
Description
A TCP connect+initialize failed with:
InvalidOperationException: Cannot start the consumer: a previous consumer thread has not yet exited.
That guard is StreamMessageConsumer.Start() (src/Daqifi.Core/Communication/Consumers/StreamMessageConsumer.cs:88-94), and the guard itself is correct — it refuses to spawn a second reader against the same stream after a prior Stop()/StopSafely() whose Join timed out, which would reintroduce framing corruption. The problem is the condition arising at all during a normal connect: it surfaces as a confusing internal error on an operation the caller has no way to retry meaningfully.
The likely path is the text-exchange consumer swap during InitializeAsync — DaqifiDevice stops the protobuf consumer, runs a text exchange, and restarts it. If the reader thread is still blocked in Stream.Read when the restart happens, Join times out and Start() throws.
Reproduction
Intermittent — 1 failure in 3 attempts, then 5 consecutive clean runs afterwards, so it is timing-dependent rather than a deterministic state issue.
Observed on a Nyquist 1 (fw 3.7.2, macOS) while connecting over WiFi to a unit that also had an active USB session, via DaqifiDeviceFactory.ConnectFromDeviceInfoAsync. Controls run immediately afterwards did not reproduce it:
| Path |
Result |
| WiFi connect via factory, nothing else open |
3/3 clean |
| USB then WiFi via factory, both open, no registry |
3/3 clean |
Same via DaqifiDeviceRegistry (#381) |
1 failure, then 2/2 clean |
The registry is not implicated — it only calls the factory, and the failure surfaced through it by chance. It handled the failure correctly (the existing USB session was left intact).
Why it is worth fixing
A spurious InvalidOperationException from a connect is indistinguishable, to a consumer, from a permanent failure. Desktop and the MCP server both surface connect errors directly to a user or agent, and "a previous consumer thread has not yet exited" gives them nothing actionable.
Suggested investigation
- Confirm whether the swap in
DaqifiDevice's text-exchange path is the source, and what Join timeout it uses.
- Consider whether a timed-out
Join should escalate (abandon the consumer instance and construct a fresh one against the new stream) rather than leaving the caller with a hard failure — a new instance has no shared reader state, so the double-reader hazard the guard protects against would not apply.
- If the timeout is simply too tight for TCP teardown, widening it may be enough, but the escalation path is the more robust fix.
Acceptance Criteria
Type: Bug
Priority: Low (intermittent; observed once)
Description
A TCP connect+initialize failed with:
That guard is
StreamMessageConsumer.Start()(src/Daqifi.Core/Communication/Consumers/StreamMessageConsumer.cs:88-94), and the guard itself is correct — it refuses to spawn a second reader against the same stream after a priorStop()/StopSafely()whoseJointimed out, which would reintroduce framing corruption. The problem is the condition arising at all during a normal connect: it surfaces as a confusing internal error on an operation the caller has no way to retry meaningfully.The likely path is the text-exchange consumer swap during
InitializeAsync—DaqifiDevicestops the protobuf consumer, runs a text exchange, and restarts it. If the reader thread is still blocked inStream.Readwhen the restart happens,Jointimes out andStart()throws.Reproduction
Intermittent — 1 failure in 3 attempts, then 5 consecutive clean runs afterwards, so it is timing-dependent rather than a deterministic state issue.
Observed on a Nyquist 1 (fw 3.7.2, macOS) while connecting over WiFi to a unit that also had an active USB session, via
DaqifiDeviceFactory.ConnectFromDeviceInfoAsync. Controls run immediately afterwards did not reproduce it:DaqifiDeviceRegistry(#381)The registry is not implicated — it only calls the factory, and the failure surfaced through it by chance. It handled the failure correctly (the existing USB session was left intact).
Why it is worth fixing
A spurious
InvalidOperationExceptionfrom a connect is indistinguishable, to a consumer, from a permanent failure. Desktop and the MCP server both surface connect errors directly to a user or agent, and "a previous consumer thread has not yet exited" gives them nothing actionable.Suggested investigation
DaqifiDevice's text-exchange path is the source, and whatJointimeout it uses.Joinshould escalate (abandon the consumer instance and construct a fresh one against the new stream) rather than leaving the caller with a hard failure — a new instance has no shared reader state, so the double-reader hazard the guard protects against would not apply.Acceptance Criteria