feat(health): consume kanata's authoritative InputGrab status (#630) - #635
Conversation
kanata's health was entirely inferential — process + TCP + stderr log-scrape + "are KeyInput events flowing." The functional check is ambiguous: no key events can mean kanata-not-grabbing, user-not-typing, OR user-on-VNC (synthetic events bypass the physical grab). The bundled fork now emits an authoritative ServerMessage::InputGrab over TCP (cd2a1e5 on keypath/bundled); this wires KeyPath to consume it as the primary, VNC-immune input-capture signal. - KanataGrabStatusStore (KeyPathCore): process-wide store of the latest InputGrab, recorded by the TCP listener and reset when the connection drops, so a non-nil value is always ground truth from the current live session. - KanataEventListener: parse the InputGrab TCP message and record it (parseInputGrab is a unit-testable static helper, like the neighbors). - ServiceHealthChecker.resolveInputCaptureStatus: layer the signal over the #632 stderr detector. STRICTLY ADDITIVE — only an authoritative active:false overrides the fallback; active:true never suppresses a stderr-detected failure (the grab bit is coarser than stderr; a cached active:true could outlive a silent grab loss). A recovery active:true clears a prior failure by falling back to the now-clean stderr. Wired into BOTH health pipelines (checkKanataServiceRuntimeSnapshot AND SystemValidator.checkHealth) so they agree on input-capture readiness. Bumps the External/kanata submodule pin to cd2a1e5. Foundation for #624 (honest VNC-aware status) and #625 (auto-recovery). Upstreaming the fork side is tracked in malpern/kanata#20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — feat(health): consume kanata's authoritative InputGrab status (#630)OverviewSolid, well-motivated PR. The belt-and-suspenders layering design is correct: Issues1.
// Current — allows: let s = KanataGrabStatusStore(); s.record(...)
public init() {}
// Should be:
private init() {}2.
3. Test class doesn't extend
Recommend extending Minor Notes
SummaryThe two actionable items are the 🤖 Generated with Claude Code |
- KanataGrabStatusStore.init() is now private (singleton footgun: prevents callers accidentally constructing a dead local instance). - Clarify that KanataInputGrabStatus.observedAt is diagnostics/logging only, not used in health decisions — staleness is structural (reset on disconnect), not age-out, so the field no longer implies a timestamp-expiry check. - Register KanataGrabStatusStore.shared.reset() in TestSingletonReset.resetAll() so every KeyPathTestCase-based test auto-isolates the global grab store, and switch KanataInputGrabConsumerTests to KeyPathTestCase for that isolation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code ReviewOverall: Well-designed PR that correctly implements the "strictly additive" signal-layering invariant. Architecture is sound, test coverage is good. One real bug to fix before merge; a few minor points. Bug: Log line in
|
…derr-only The "Kanata service check complete" log still printed stderrDiagnosis.inputCapture.isReady after the resolved value moved to `inputCapture`. In the exact VNC-masked grab-failure case this PR catches (stderr clean, InputGrab active:false), the log would read inputCaptureReady=true while the snapshot records false — misleading any debugging session. Log the resolved value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — feat(health): consume kanata's authoritative InputGrab status (#630)OverviewThis PR closes the consumer half of the VNC-immune health signal story: kanata now emits Correctness ✅
guard let grab = KanataGrabStatusStore.shared.latest, !grab.active else {
return stderrFallback
}Exits (falls back) on nil store or Reset sites are correct and idempotent. Both Both health pipelines are now wired. The fix to route Issues / Suggestions1.
|
…signal (#625) (#644) * feat(lifecycle): auto-recover degraded kanata via authoritative grab signal (#625) When kanata comes up degraded — TCP/VHID ready but it failed to seize the keyboard exclusively (restart race, driver crash, another app holding the grab) — remapping is silently dead with no retry until a forced clean restart. This wires bounded auto-recovery onto the authoritative `InputGrab` signal (#630/#635/#637): kanata emits `active=false`+reason on a grab failure. The event flows KanataEventListener → .kanataGrabStatusChanged notification → RuntimeCoordinator.handleGrabStatusChanged, which consults the bounded guard (ServiceHealthMonitor.decideGrabRecovery: 3 attempts / 300s episode window / give-up) and runs RecoveryCoordinator.attemptKeyboardRecovery. `active=true` resets the budget. Gated two ways so it never fires spuriously: - Benign `active=false` during an intentional stop is suppressed via a depth-counted transition gate on ServiceLifecycleCoordinator (scoped to the STOP phase only, so a genuine post-start grab failure is still caught). - Single-flight `isRecoveringGrab` prevents overlapping recoveries (a recovery is a ~5s kill→restart that itself emits more grab-status events). The pure gate decision is extracted (decideGrabRecoveryGate) and unit-tested so the suppression rules are covered without touching the real recovery action. This is part-2 of #625 (detection + recovery). The stop→start race fix (wait-for-exit before start) is a separate follow-up. Also fixes a stale test left red on master by #642: WizardPureLogicTests test_inspect_inputCaptureNotReady_producesIMIssue now supplies a permission-type capture reason, matching #642's honest-attribution contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#625): clear stop-grace on start so post-restart grab failures aren't suppressed Addresses Codex P2: restartKanata = stop (arms 2s grace) + start, so the grace bled into the new daemon's startup and a genuine InputGrab active=false from the freshly started kanata was misclassified as a benign transition. startKanata now clears the lingering stop-grace at entry — the grace only needs to cover the OLD process's last gasp, which is past once a start begins. Covers all stop->start paths (manual restart, device-selection restart, recovery's own restart). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Wires KeyPath to consume the bundled kanata fork's new authoritative
InputGrabTCP status as the primary, VNC-immune input-capture health signal. Closes the consumer half of #630.Today kanata's health is entirely inferential — process + TCP + stderr log-scrape + "are
KeyInputevents flowing." The functional check is ambiguous: no key events can mean kanata-not-grabbing, user-not-typing, or user-on-VNC (synthetic events bypass the physical grab — the long misdiagnosis from last session). kanata itself knows the ground truth: it just succeeded or failed at seizing the keyboard. The fork now emits that over TCP ({"InputGrab":{"active":bool,"devices":[...],"reason":...}}, commitcd2a1e5onkeypath/bundled); this PR consumes it.What's here
KanataGrabStatusStore(KeyPathCore): process-wide store of the latestInputGrab, recorded by the TCP listener and reset when the connection drops, so a non-nil value is always ground truth from the current live session.KanataEventListener: parses theInputGrabmessage and records it (parseInputGrabis a unit-testable static helper, matching the existingextractMessagePushMessages/normalizedCapabilitiespattern).ServiceHealthChecker.resolveInputCaptureStatus: layers the signal over the fix: detect kanata 'up but not grabbing the keyboard' so status stops lying (#624) #632 stderr detector, strictly additive — only an authoritativeactive:falseoverrides the fallback;active:truenever suppresses a stderr-detected failure (the grab bit is coarser than stderr; a cachedactive:truecould outlive a silent grab loss). A recoveryactive:trueclears a prior failure by deferring to the now-clean stderr. Wired into both health pipelines (checkKanataServiceRuntimeSnapshotandSystemValidator.checkHealth) so they agree.cd2a1e5.Design notes (belt-and-suspenders)
The authoritative signal can only make status more truthful (catch a VNC-masked failure stderr missed), never less. kanata does not replay
InputGrabon connect, so on a fresh connect to an already-grabbing kanata the store stays empty and the #632 stderr detector remains the backstop — exactly the intended layering.Review gate
Ran the mandatory adversarial review (correctness + concurrency + cleanup/altitude angles). It caught two real issues that are fixed in this PR:
SystemValidator.checkHealth(a primary production pipeline feedingSystemSnapshot/SystemInspector/InstallerEngine) bypassed the new signal. Now routed through the same chokepoint.active:truemask a stderr-detected failure / go stale. Fixed by making the overrideactive:false-only (strictly additive).Also removed a speculative unused NotificationCenter broadcast the review flagged.
Testing
KanataInputGrabConsumerTests(parse, store record/reset, resolver precedence incl. the no-mask + recovery invariants, end-to-end decision).KanataInputGrabDetectionTests(fix: detect kanata 'up but not grabbing the keyboard' so status stops lying (#624) #632) andServiceHealthCheckerTestsstill green.Follow-ups
malpern/kanata#20(needs Linux/Windows emit sites + aRequestInputGrabquery path).🤖 Generated with Claude Code