Runtime, monitors and Explorer recovery

There is no support gate: the discovered clock candidate always places the clock after geometric validation. A failed, late or stale discovery fails closed to an authoritative empty batch, which removes the overlays and leaves the native clock untouched.

Components

ItemWhereContract
Runtime<P: Platform>clock-windows/src/runtime.rsSingle coordinator between platform inputs and the core reducer. No second state machine. Pure of OS access; 30 deterministic tests with a scripted platform (plus shell, protocol, gate, candidate, event and time tests).
NativePlatform, runruntime/native.rs, runtime/overlay_window.rsOne UI thread: hidden top-level host window, SetTimer one-shots, WinEvent hooks, session notifications, overlay windows, Direct2D, Settings dialog.
eventsevents.rs, events/native.rsWindow message / WinEvent → Signal; coalescing Pending; bounded TransitionPoll. Callbacks only queue.
instanceinstance.rs, instance/native.rsLocal\ElevenClock-rs.Instance.<SID> mutex, HOST_CLASS/title lookup, WM_SHOW_SETTINGS, Role/LaunchMode.
taskbar::{protocol,worker,helper,candidate}taskbar/Wire contract, supervised worker, same-executable helper, candidate selection. See ADR 0002.
clock_timeclock_time.rsOne fresh zone/DST-resolved DateTime<FixedOffset> per update.
App lifecycleclock-app/src/app.rs, settings.rs, settings.rcModeless Settings; close keeps running; Exit button; second launch focuses.

Event-to-core mapping

SourceSignalRuntime action → core event
WM_DISPLAYCHANGE, WM_DPICHANGED, WM_SETTINGCHANGE (“TraySettings” or SPI_SETWORKAREA), WTS console/remote connectTopologyChanged250 ms trailing settle, then TopologyChanged (one discovery per burst; one follow-up if topology changed while one was in flight)
WM_THEMECHANGED, WM_SYSCOLORCHANGE, WM_SETTINGCHANGE (ImmersiveColorSet, other)AppearanceMaybeChangedre-read system appearance + font; ThemeChanged{revision} only on a real change
WM_TIMECHANGETimeChangedfresh local snapshot → TimeChanged
WM_POWERBROADCAST suspend / resumeSuspended / Resumedresume: TimeChanged, shell check, appearance, topology
WM_WTSSESSION_CHANGE lock / unlockSessionLocked / SessionUnlockedSessionLocked; unlock: SessionUnlocked (reducer requests discovery) + fresh TimeChanged + shell check
registered TaskbarCreatedTaskbarCreatedforced shell re-identification → ExplorerLost (generation + 1, old overlays destroyed, new discovery)
WinEvent show/hide/location (Explorer PID only, Shell_*TrayWnd only)Shell(TaskbarChanged)start bounded transition polling; observe → VisibilityChanged / rediscover
WinEvent foreground / minimize-end / move-size-end (system-wide, low rate)Shell(ForegroundChanged)observe → FullscreenChanged per monitor
WM_ENDSESSION, WM_CLOSE on host, Settings ExitEndSession / exit flagQuit
WM_TIMER (clock one-shot)—one fresh snapshot → Tick; reducer returns ArmTimer
WM_TIMER (poll one-shot)—shell identity check, due retries / settle, observation pass
worker reportDiscovery(report)accept only the current generation and matching shell identity; failure → authoritative empty batch (fail closed) + backoff

Fallback polling rates and timers

Shutdown and failure semantics

Quit destroys every overlay (reducer effects), cancels both timers, unhooks every WinEvent hook, unregisters session notifications, terminates and reaps any helper, joins the worker (bounded), destroys the host window and unregisters classes. A panic in a window procedure or hook becomes a clean shutdown with an error box. A crash removes all windows and (via the kill-on-close job) any helper. Explorer is never modified, injected into or parented to, so the native clock is always usable.

Lifecycle contract

Interactive launch (Start menu, double-click) opens Settings; login startup (--startup, the marker the startup registration uses) runs quietly. A second launch posts WM_SHOW_SETTINGS to the existing host window (lookup by class and SID-derived title, bounded retry for simultaneous launches) after AllowSetForegroundWindow, and exits 0 without creating a clock. Closing Settings (Close, Escape, X, system menu) destroys only the dialog. Exit ElevenClock in Settings or the tray/clock menu, a polite WM_CLOSE to the host, or session end quits.

What the tests prove (and do not)

UIA lifetime

tests/uia_lifetime.rs (custom harness; CI sets ELEVENCLOCK_RS_UIA_EXPLORER=1 and ELEVENCLOCK_RS_EVIDENCE_DIR) records raw per-call HRESULTs for a healthy window, a provider that never pumps (hits the 4 s deadline with one helper spawned and reaped), a provider killed mid-call, elements retained across provider death (UIA_E_ELEMENTNOTAVAILABLE) and a real Explorer suspended and killed mid-call.

What shaped the design: a UIA call in flight when the shell dies is not guaranteed to fail with a useful HRESULT (an earlier attempt returned S_OK), so the parent never trusts a late or successful-looking helper result — it relies on the generation/shell-identity check and the helper deadline. Retained elements always fail 0x80040201, so no element is cached across a generation. On the hosted Server image Windows did not restart the shell after taskkill; the harness started explorer.exe itself (shell_started_manually: true, no taskbar for 45 s), so this proves correlation and stale-object behaviour, not Windows’ own shell auto-restart timing.

Known limitations

Parked and late taskbars

Parked taskbars are classified as hidden even when their rectangle equals the discovery snapshot. Each taskbar is observed independently; revealing a taskbar whose clock could not be validated schedules fresh discovery, including a reveal with unchanged window bounds. Offscreen clock candidates remain rejected.

The observation pass also compares the current shell’s top-level taskbar handles with the last discovery batch. This cheap native inventory uses no UI Automation and detects a secondary taskbar created after a partial Explorer-startup batch, even if its event was missed. Requests share the existing two-second throttle, settle window and worker; failed discovery retains its exponential backoff. Overlay DPI/display messages now queue topology signals as well as invalidating pixels. Stale geometry releases the cover before the bounded helper finishes.

Visible, selected, on-monitor clocks that remain unverified after a successful batch get four bounded settling retries (2, 4, 8, 16 seconds); unsupported layouts do not create an endless discovery loop.

Optional update checks

The runtime supplies managed mode, lock/pause/suspend/exit eligibility, a resume sequence, UTC seconds and monotonic time to the portable UpdateRuntime policy. The UI-thread update service owns the single worker and uses the Settings session for serialized persistence even when no Settings dialog exists. A dedicated one-shot host timer wakes at the next opportunity, with at least three minutes of startup or resume grace and at least 24 hours between attempts. No timer is armed by default or in managed mode; existing clock and shell timers do not initiate requests.

Both manual and automatic requests use the same bounded worker and coordinator. Lock, suspend and pause cancel work and remove the automatic timer. Turning consent off cancels automatic work; Exit cancels either kind. Cancelled workers retain their in-flight slot until their bounded completion, preventing overlap, and cannot notify. Shutdown drops the worker without joining network operations. Explorer replacement does not reset the update schedule or start a check. Rate-limit deadlines and notification markers survive restart; failed automatic checks are quiet. See ADR 0003 and preferences.