Repository navigation
Description
Problem
Windows ARM VM keyboard switch capture stops responding despite reporting Ready to begin. Space briefly resumed scanning after opening and closing switch editing, then the UI reported “Release held keys before starting capture: Space” after release. PR #777 is included in the affected build, PR #790 at 3713b87.
Keep this fix in v1.0.0-rc.12, separate from keyboard issue #789 and PR #790, moved to v1.0.0-rc.13.
Investigation findings
- Existing Space → Select / hold → Reverse settings are valid and unchanged.
- Reproduced failure in the running Windows ARM VM, including a successful scan between failures. Thus this is intermittent capture failure, not simply an absent mapping.
- A read-only GetAsyncKeyState(VK_SPACE) check in the interactive Windows user session returned SpaceDown=False while the application displayed the held-key error. This observation alone is not proof of physical release because suppressed input affects asynchronous state.
switch_input/mod.rs, Capture::begin, rejects cached core.physical/native.held_keys before reaching replacement or fresh native reconciliation. A lost release can therefore strand retry behind ownership belonging to the broken hook.windows_hook.rsdispatch keeps updating worker_ms even without hook callbacks. This checks worker progress, not hook installation.windows.rsregisters Raw Input but handles only device removal; it does not currently use input delivery to diagnose missing hook callbacks.- Microsoft documents that a timed-out WH_KEYBOARD_LL hook can be silently removed. This is a plausible trigger, not a confirmed cause in this VM: https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc
Proposed investigation and fix direction
- Add bounded, non-text native diagnostics for assigned-key hook delivery and independent Raw Input observations. Keep Raw Input out of gesture execution. Establish whether hook removal, missed release, or VM delivery is responsible.
- Reproduce capture loss both before any press and after a consumed down. Do not infer hook health from idle worker heartbeats.
- On confirmed loss, cancel the generation and pending actions, rebuild capture, and reconcile stale ownership only using a validated recovery sequence. No synthetic Select/release gesture and no unconditional clearing of physically held keys.
- Verify pre-held keys, repeats, cancellation, emergency stop, generated shortcuts, focus changes, long idle and VM suspend/resume. Test suppression and recovery in the Windows ARM VM with disposable content.
No product code has changed yet. Investigation branch: codex/windows-capture-recovery from current main 7e383f4. Preserve settings, BLE, Mac capture and the signed local Mac app. Independent latest-head review and required checks are required before delivering an implementation PR; merge/release remain separate actions.
Implementation qualification — blocked, 2026-09-18
A standalone native ARM64 Rust probe registered a message-only window with RIDEV_INPUTSINK and a WH_KEYBOARD_LL hook. It counted only Space press/release events in the hook and WM_INPUT; no typed text was logged. The hook either returned 1 for Space or passed it to CallNextHookEx. Registration and hook installation were asserted successful.
Repeated with Switchify stopped and exactly one probe in interactive session 1, using the same VM input source:
| Mode | Hook down | Hook up | Raw down | Raw up |
|---|---|---|---|---|
| Suppression | 1 | 1 | 0 | 0 |
| Pass-through | 1 | 1 | 1 | 1 |
This fails the accepted plan’s explicit requirement to qualify Raw Input release visibility during suppression. Raw Input cannot be used as the independent release confirmation assumed by that plan. The test does not establish the original intermittent capture-loss cause. Independent review confirmed the probe and blocker; its recommendation to isolate the app/probe was completed with the results above.
Per the accepted qualification gate, no polling substitute, unconditional held-state reset, product code, or fix PR has been introduced. A revised recovery design must establish hook health and safely consume a fresh recovery gesture without depending on suppressed Raw Input events.
Evidence retained inside the Windows VM at C:\Users\owenmcgirr\source\capture791-probe: src/main.rs, isolated-suppressed.txt, and isolated-passthrough.txt. Temporary probe process/tasks and transfer server removed. Existing PR790 build restored running; VM settings unchanged and signed local Mac app untouched.
Revised recovery design
Use Raw Input only as positive evidence of unexpected pass-through, never as proof that a suppressed key was released. No periodic synthetic health-check keystrokes and no polling-based ownership reset.
- Maintain bounded per-key hook observations for assigned keys and Escape, including whether each edge was deliberately passed or consumed. Exclude Switchify-generated markers before changing physical or recovery state. Correlate Raw Input only against deliberately passed observations, allowing up to the existing 1.5-second watchdog interval for delivery ordering. An unmatched assigned-key Raw Input event while capture is expected to suppress it signals capture loss. Expected pre-held releases and cancellation pass-through must not cause false faults.
- On confirmed loss, invalidate the gesture generation and cancel queued actions, holds and countdown clicks before any rearm. Use the existing cleanup and recovery HUD. The key event that exposed failure may already have reached the foreground app; do not replay or undo it and do not claim uninterrupted suppression across failure.
- Reinstall the native hook on its message-loop thread without waiting for stale ownership to clear. Transfer uncertain held-key ownership into a recovery quarantine. Establish the replacement hook before retiring the old one; if installation fails, keep actions disabled and retry at the existing two-second interval. Ensure old and replacement callbacks cannot deliver duplicate actions.
- The replacement hook suppresses assigned keys during recovery. A complete press/release observed by this hook is consumed solely for recovery; an initial release without a new press may reconcile an uncertain key but cannot itself complete the recovery gesture. Every previously uncertain key must also have an observed release before actions resume. Repeats cannot execute or complete recovery. Keep Escape and emergency-stop behavior available.
- Show “Keyboard capture interrupted. Release your switches, then press and release a switch to reconnect.” On successful recovery return to idle, waiting for a separate Select press. Learning must return a retryable capture error rather than learn the recovery key.
- Worker heartbeats remain worker watchdogs only. No-input periods do not establish whether Windows silently removed a hook. Detect on the next observable input or known native failure.
Qualification before implementation: use an isolated VM probe to remove a hook deliberately before a press and between down/up, verify unexpected Raw Input visibility, replacement-hook suppression and recovery-gesture capture. Add bounded-ledger ordering/overflow tests and no-action recovery tests. Then validate foreground/background/minimized operation, held keys, repeated recovery, generated shortcuts and suspend/resume in disposable VM content. Preserve settings and restore prior VM app/state. Required frontend/Rust checks, independent latest-head review, ready PR and passing CI remain required.
Implementation and qualification update
PR #792 at c1dbf2e now isolates native capture in a same-executable worker. VM experiments established that the in-process hook lost callbacks with Switchify's WebView focused; sole-worker qualification and full-app tests restore foreground capture. Native hook recovery retains its ownership quarantine. Worker/IPC failure instead latches capture off until restart after releasing switches, because lost ownership cannot safely be inferred from async key state.
Local required checks and independent latest-head review pass. Windows ARM foreground/minimized suppression, worker crash handling, parent-exit cleanup and interactive overlay smoke tests passed. VM settings were restored byte-for-byte and the previous PR #790 build is running; signed local Mac app untouched.
Release qualification is still blocked on signed UIAccess self-spawn and remaining signed native hold/generated-shortcut tests. The VM currently has zero available code-signing certificates, no configured signing thumbprint and no running SimplySign session. PR remains draft; current-head CI is running. Full-app hook fault injection was inconclusive and is not claimed as passing.
Activity