Skip to content

fix: control v2 disconnected instantly, then shadow mode detached the viewer - #139

Merged
FelineStateMachine merged 2 commits into
mainfrom
fix/portal-control-v2-input-ack
Jul 25, 2026
Merged

fix: control v2 disconnected instantly, then shadow mode detached the viewer#139
FelineStateMachine merged 2 commits into
mainfrom
fix/portal-control-v2-input-ack

Conversation

@FelineStateMachine

@FelineStateMachine FelineStateMachine commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Two defects found during the v0.1.0 end-to-end test against a live Bazzite host (#137, #140). The second only became visible once the first was fixed.

1. Control v2 disconnected on its first input ack

set_input_ack_negotiated is called only inside the if control_protocol == ControlProtocol::V1 branch. Control v2 negotiates InputAck with the host but never records it, so observe_ack saw negotiated == false and rejected the host's first acknowledgement:

Host sent an input acknowledgement that was not negotiated

That marks pointer feedback terminal/malformed, which the frontend treats as fatal → full disconnect. The host sends that ack the instant the input stream opens, to prime pointer feedback, so control v2 could never have worked.

Both rejection paths in run_input_feedback_v2 also broke silently, so the fatal disconnect left nothing in the log to explain itself — which is why it presented as a transport fault. They now name the mismatch.

2. Shadow mode detached viewers it had disabled the remedy for

With the session surviving, it then died after ~25s with reason="adaptive_recovery_detach".

RecoveryMutation::Detach applied unconditionally. In shadow mode the controller computes a lower bitrate and never applies it, so a pressured viewer cannot recover by construction and the recovery machine escalates to detach on schedule.

This is the documented default: docs/sigil-host-activation.md step 5 writes encoder_backend = "external-gst-launch", which is exactly what selects shadow mode. Detaching is an action, so it's now gated on actuation being available, and logs when it declines.

Also collapses the duplicated actuation predicate into one encoder_actuation_available helper.

Verified

Fix 1 against the live host: input v2 accepted and the session survives, versus dying ~8ms after accept.

Fix 2 has a regression test pinning that the activation guide's own backend yields shadow mode and never gains detach permission.

fmt, clippy -D warnings, 473 tests green.

Clicking to control always dropped the whole session and forced a reconnect.

`set_input_ack_negotiated` is called only inside the
`if control_protocol == ControlProtocol::V1` branch in connect.rs. The control
v2 path negotiates InputAck with the host but never records that in the session
diagnostics, so `observe_ack` still saw `negotiated == false` and rejected the
host's first acknowledgement with "Host sent an input acknowledgement that was
not negotiated". That marks the pointer feedback terminal/malformed, and the
frontend treats a terminal feedback envelope as fatal and disconnects.

The host emits that first ack the moment the input v2 stream opens, to prime
pointer feedback, so every control attempt hit it. Control v2 could not have
worked.

Both rejection paths in `run_input_feedback_v2` also broke silently. A fatal
disconnect left nothing in the log to explain itself, which is why this looked
like a transport fault: the host reported only "connection lost" once Portal
had already torn the endpoint down. They now name the mismatch.

Verified against a live Bazzite host running the published 0.1.0: input v2 is
accepted and the session survives instead of dying ~8ms later, and the
rejection no longer appears.
…help

With the input-ack disconnect fixed, the session survived long enough to reach
the next failure: the host detached the viewer after ~25 seconds with
reason="adaptive_recovery_detach".

`RecoveryMutation::Detach` applied unconditionally. In shadow mode the
controller computes a lower bitrate and never applies it, so a viewer under
sustained pressure cannot recover -- the only remedy is switched off -- and the
recovery state machine escalates to detach on schedule. Shadow mode disabled the
remedy and kept the punishment.

This is the documented default, not an exotic configuration.
`docs/sigil-host-activation.md` step 5 writes
`encoder_backend = "external-gst-launch"`, which is exactly what selects shadow
mode, so a host provisioned by following the guide detaches any viewer that
experiences sustained pressure. On the reference host that is 2560x1600@60 at
12000 kbps to a Wi-Fi client.

Detaching is an action, so it is now gated on actuation being available, and
says so when it declines. The duplicated actuation predicate becomes one
`encoder_actuation_available` helper used by both the coordinator and the
feedback session.
@FelineStateMachine FelineStateMachine changed the title fix(portal): control v2 disconnected the session on its first input ack fix: control v2 disconnected instantly, then shadow mode detached the viewer Jul 25, 2026
@FelineStateMachine
FelineStateMachine merged commit ad308aa into main Jul 25, 2026
15 checks passed
@FelineStateMachine
FelineStateMachine deleted the fix/portal-control-v2-input-ack branch July 26, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant