Skip to content

Reliability: zero-window keepalive + overlay/log hardening; TS7 + oxlint/oxfmt (0.0.50) - #55

Merged
gyng merged 3 commits into
mainfrom
fix/zero-window-keepalive
Jul 9, 2026
Merged

Reliability: zero-window keepalive + overlay/log hardening; TS7 + oxlint/oxfmt (0.0.50)#55
gyng merged 3 commits into
mainfrom
fix/zero-window-keepalive

Conversation

@gyng

@gyng gyng commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Reliability (the "didn't autostart" incident, 2026-07-10)

Root cause: widgetsack is a tray app, but Tauri's default exits when the last window closes — and zero windows is reachable by design (an empty-primary `main` self-destructs to reclaim its renderer, leaving layouts with one populated secondary monitor hanging on a single overlay window). Any transient spawn failure ended the process silently.

  • keepalive.rs — `ExitRequested { code: None }` is prevented; a hidden `main` respawns on a 30s retry until the reconcile cycle succeeds. Tray Quit (`app.exit(0)`) still exits.
  • displaywatch.rs — hidden-window `WM_DISPLAYCHANGE` watcher respawns `main` instantly when a monitor returns at zero windows (fast path over the 30s retry). Hidden top-level window, not `HWND_MESSAGE` — message-only windows don't receive broadcasts.
  • log_client — overlay lifecycle failures (reconcile/spawn/self-fit) now persist to the rotating log file (target `client`) instead of dying with the webview console.
  • reconcile hardening — a `load_layout`/parse failure now skips the reconcile pass instead of being treated as "layout is empty" (which closed every working overlay); `reconcileOverlays` is single-flight with a trailing rerun (new pure `core/singleFlight.ts`).
  • GSMTC — media-session-manager failure degrades gracefully (media disabled) instead of a startup `.expect` crash; consumer loop can no longer hot-spin on a closed channel.
  • `cargo update` (24 crates; no tauri/wry/softbuffer bumps available — upstream softbuffer `!dc.is_null()` panic remains open). Version 0.0.50.

Toolchain: TypeScript 7 + oxlint + oxfmt

  • typescript 7.0.2 (native): zero code changes; `tsc --noEmit` ~8.0s → ~1.7s.
  • typescript-eslint has no TS7 support (peer `<6.1.0`, hard-crashes) → oxlint 1.73 with equivalent tiers; zero-warnings gate unchanged. Kept: `no-explicit-any`, `rules-of-hooks`, `exhaustive-deps`. Known loss: 14/16 react-hooks v7 (React Compiler) rules have no oxlint equivalent yet — the Dependency upgrade (ESLint 9 / Vite 8 / Vitest 4 / TS 6 / Prettier 3) + react-hooks v7 fixes + autostart Run-key fix (0.0.49) #54 fixes remain in code, unenforced for now.
  • oxfmt 0.58 replaces Prettier (same style: tabs/single quotes/no trailing commas/width 100); covers all previously-formatted file types; one-time reformat was 4 files, whitespace-only.

Verification

  • `cargo test` 200 (4 new) / clippy / fmt clean; tsc clean; oxfmt+oxlint clean; 2768 unit tests; build; `check:docs`; 34 Playwright e2e.
  • Runtime-verified (debug `--multi` instance): survives repeated zero-window cycles and recovers the overlay when a populated monitor returns; `log_client` mechanism validated against the production-proven `log_diag` path.

🤖 Generated with Claude Code

gyng and others added 3 commits July 10, 2026 02:33
…pawn main

widgetsack is a tray app, but Tauri's default exits the process when the last
window closes. Zero windows is reachable BY DESIGN: an empty-primary `main`
self-destructs to reclaim its renderer (overlay.ts setMainWindowVisible), so a
layout whose widgets all sit on one secondary monitor lives on a single overlay
window — and any transient failure to spawn it (monitor not yet enumerated at
logon, WebView2 hiccup, monitor DDC-switched away) silently ended the process.
This was the "didn't autostart" failure of 2026-07-10.

- keepalive.rs (new): should_prevent_exit pure seam (+tests); on_zero_windows
  schedules a rate-limited (30s) respawn of the hidden `main`, whose own Canvas
  init re-runs the reconcile cycle — spawning overlays for whatever monitors
  exist NOW, then self-destroying again if the primary is still empty. Retries
  pace themselves off the event cycle (no polling); one attempt in flight;
  self-reschedules if window creation itself fails.
- main.rs: .run(context) → .build(context).run(handler); ExitRequested with
  code None is prevented. Tray Quit (app.exit(0) → code Some(0)) still exits.
- command.rs: respawn_main_hidden made pub(crate) + a `reason` log tag shared
  by the layout-watcher and keepalive recovery paths.

Runtime-verified with a debug --multi instance on a layout whose only populated
monitor key matches no connected display: the app survives repeated zero-window
cycles (keepalive → respawn → self-destroy) and recovers the overlay as soon as
the layout regains a real monitor. cargo test (200 incl. 2 new), clippy, fmt
all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
….0.50)

Follow-ups to the zero-window keepalive, from the post-incident review:

- log_client command (command.rs) + overlay.ts logClient helper: overlay
  lifecycle failures (reconcile, spawn, self-fit, main destroy/recreate) now
  land in the persistent rotating log file as target "client" instead of dying
  with the webview console — the 2026-07-10 postmortem cost hours because these
  paths were console-only.
- populatedMonitorKeys returns null on a load_layout/parse failure and
  reconcileOverlays skips the pass entirely — "couldn't read the layout" is no
  longer treated as "layout is empty" (which closed every working overlay on a
  transient IPC hiccup).
- reconcileOverlays is single-flight with a trailing rerun (new pure
  core/singleFlight.ts + tests): concurrent passes (init, layout_changed,
  topology watcher) no longer race to create the same overlay label.
- displaywatch.rs: hidden-window WM_DISPLAYCHANGE watcher respawns `main` the
  moment a monitor returns while the app is at zero windows — instant recovery
  instead of keepalive's 30s retry. (Deliberately a hidden top-level window,
  not HWND_MESSAGE: message-only windows don't receive broadcast messages.)
- GSMTC SessionManager failure now degrades gracefully (media disabled, app
  starts) instead of a startup .expect crash; the rx consumer became
  `while let` so a closed channel can't hot-spin.
- cargo update (24 crates within semver; no tauri/wry/softbuffer bumps were
  available — the upstream softbuffer !dc.is_null() panic remains open).
- Version 0.0.50.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- typescript 6.0.3 -> 7.0.2 (native compiler): zero code changes needed;
  tsc --noEmit drops from ~8.0s to ~1.7s (fast mode; a bimodal ~6.3s mode —
  suspected Defender scanning the native binary — still beats TS6).
- typescript-eslint has no TS7 support (peer <6.1.0, crashes on the changed
  API), so lint moves to oxlint 1.73 (.oxlintrc.json): typescript/unicorn/oxc/
  react plugins, correctness category + explicit overrides mirroring the old
  recommended tiers. Zero-warnings gate unchanged (--max-warnings=0).
  Must-keeps preserved: typescript/no-explicit-any, react/rules-of-hooks,
  react/exhaustive-deps. KNOWN COVERAGE LOSS: 14 of 16 react-hooks v7
  (React-Compiler-era) rules have no oxlint equivalent yet — the #54 fixes
  stay in code but are no longer machine-enforced. Also lost: react/prop-types
  (moot under TS), react/no-deprecated.
- Formatting moves from Prettier to oxfmt 0.58 (.oxfmtrc.json via
  --migrate=prettier: tabs, single quotes, no trailing commas, width 100).
  Covers TS/TSX/JS/JSX/CSS/JSON/MD/HTML — no lost coverage; the one-time
  reformat touched 4 files, whitespace-only.
- eslint-disable comments renamed to oxlint-disable (tool-assisted, 11 files);
  legacy @typescript-eslint/* rule names inside them still resolve. A handful
  of newly-surfaced correctness findings fixed (useless spread fallbacks,
  new Array(n) holes, unsafe ?. in tests).
- Removed devDeps: eslint, @eslint/js, typescript-eslint, eslint-plugin-react,
  eslint-plugin-react-hooks, eslint-config-prettier, globals, prettier.
- AGENTS.md formatting/linting section updated.

Gates: tsc clean, oxfmt+oxlint clean, 2768 unit tests, build, check:docs,
34 Playwright e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gyng gyng changed the title fix(keepalive): survive zero windows — prevent last-window exit + respawn main Reliability: zero-window keepalive + overlay/log hardening; TS7 + oxlint/oxfmt (0.0.50) Jul 9, 2026
@gyng
gyng merged commit 7ce96f0 into main Jul 9, 2026
4 checks passed
@gyng
gyng deleted the fix/zero-window-keepalive branch July 9, 2026 20:00
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