The idle notification scheduler was initialized with platform 'unsupported'
before platform detection had run. This stale value was passed to send/playSound
callbacks, causing sendSessionNotification to silently no-op (no switch case for
'unsupported'). Session-idle notifications never fired as a result.
Fix: remove platform from scheduler options entirely. Callbacks now resolve
platform via ensureNotificationPlatform() which is sync, cached, and already
called by each event handler before scheduling.
Add cmux as the highest-priority notification provider on macOS.
cmux delivers notifications via native UNUserNotificationCenter,
properly attributed to the cmux app instead of Script Editor.
Notification priority: cmux > terminal-notifier > osascript
Tests cover the full fallback chain:
- cmux available: uses cmux, skips others
- cmux fails: falls back to terminal-notifier
- cmux + terminal-notifier fail: falls back to osascript
- cmux not available: skips to terminal-notifier
Fixes#3628
Root cause: the context-window-monitor hook computes actualUsagePercentage = (input + cache.read) / actualLimit and renders both 'X% used' and '(1 - X) * 100% remaining' inside a [SYSTEM DIRECTIVE: OH-MY-OPENCODE - CONTEXT WINDOW MONITOR] block that is appended to bash tool output. When resolveActualContextLimit() underestimates the model's real context window (for example a 1M-context Anthropic model that falls back to the 200K default per #3450), totalInputTokens > actualLimit and the rendered numbers go nonsensical (issue #3655 reproduces 144.7% used / -44.7% remaining at 289,370 / 200,000 tokens). Safety-tuned models recognize the >100% / negative-remaining pattern as a tell-tale prompt injection and refuse to follow the directive.
Fix: clamp actualUsagePercentage to [0, 1] before formatting. The 70% threshold check still uses the raw value so the block continues to fire above threshold, and resolveActualContextLimit() is left untouched (the deeper resolver concern is tracked separately as #3450). When totalInputTokens exceeds actualLimit the displayed numbers now read '100.0% used / 0.0% remaining' instead of the impossible >100% / negative pair, and safety-tuned models stop flagging the block as an injection attempt.
Verification: added a regression test (input 289,370, limit 200,000) that asserts usedPct in [0,100] and remainingPct in [0,100]. Test fails before the fix (Received: 144.7) and passes after. Full context-window-monitor.test.ts and context-window-monitor.model-context-limits.test.ts: 15 pass / 0 fail. Typecheck clean.
The zauc-mocks-hook variant previously asserted that session.created synchronously
ran the startup checks. After auto-update-checker was refactored to defer work to
the first session.idle via scheduleDeferredIdleCheck (5s timer), those assertions
never fired.
Mirror the mock+capture pattern from hook.test.ts so the test drives the deferred
callback synchronously, preserving the original invariants (hasChecked guard,
localDev toast, sisyphus wording) without waiting on real timers.