Skip to content

fix(g6): hidden windows stop rendering; judge leaves VRAM between cycles; DB budget - #967

Merged
runyourempire merged 3 commits into
mainfrom
fix/g6-idle-cpu
Oct 17, 2026
Merged

runyourempire merged 3 commits into
mainfrom
fix/g6-idle-cpu

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Launch gate G6 (resources): idle CPU, judge VRAM, DB budget.

1. Hidden windows stop rendering (idle CPU root cause)

Tauri's hide() and a minimise hide only the native window. wry sets the WebView2 controller's IsVisible once, at creation, and never changes it on a window-level hide. Measured on a debug build after a toast and the desktop brief had been shown and hidden again:

  • Both hidden webviews still reported visibilityState "visible".
  • requestAnimationFrame ran at the display rate (241 fps).
  • Both kept their infinite animations running: the toast's .priority-dot.alert dot-pulse and the brief's .synthesis-text pulse-opacity.
  • Cost: two renderers at ~3% of a core each and the GPU process at 4.6% (13.0% total).
  • Causal check: pausing just those animations through eval_js dropped both renderers to 0% and the GPU process to 0.05%.

Fix: new src-tauri/src/webview_visibility.rs, a plugin registered once in lib.rs.

  • It subclasses every webview's host window and mirrors WM_SHOWWINDOW and minimise/restore into controller.SetIsVisible.
  • This covers every hide path without call-site edits: the toast and brief timers, close-to-tray and minimise.
  • document.hidden now tells the truth, which src/lib/motion-gate.ts already assumed.
  • A window's initial state is left alone; only a real show or hide changes it. The plugin is inert on macOS and Linux.
  • Verified live on a debug build: minimise, restore, hide and show each flip the main document's visibility, and the window renders normally afterwards (screenshot). The toast reads visible while on screen and hidden with 0 rAF frames once dismissed.

2. Judge VRAM

  • OLLAMA_KEEP_ALIVE drops from 30m to 5m. That is long enough to bridge the judge lanes of one cycle and shorter than the ~10 min between cycles. It applies to every 4DA request to Ollama (briefs, digests, search, judge lanes), not only the judge; 5m is also Ollama's own default.
  • The cycle's last judge lane (run_pending_verdict_drain, used by both the GUI scheduler and the headless engine) now unloads the routed local judge (local_judge::release_after_cycle).
  • Never unloaded: a model the user chose (main provider Ollama: may_route_local is false, so nothing is released, tested), the model while any judge lane in this process is still running (a LaneGuard from refresh_if_stale counts in-flight lanes, tested), and anything when the user set OLLAMA_KEEP_ALIVE (reuses llm::ollama_keep_alive_for).
  • A second process (the headless engine) cannot see this process's lanes. If its unload lands between this process's warm-up and a timed call, Ollama's reported load_duration is subtracted from that call's time before the breaker judges it (local_judge::observe_load, tested), so a reload never trips SLOW_CALL.
  • Measured (gemma4:12b, RTX 4080 SUPER): about 8.4 GB of VRAM freed between cycles. Each cycle pays one reload: 6.5–9.3 s from the OS file cache, 36 s from disk. A warm call takes ~0.3 s.
  • The reload happens in local_judge::warm before each lane, outside the breaker's timing.

3. G6 DB budget

  • G6_DB_BUDGET_BYTES = 500 MiB for the projected steady-state DB at default retention (30 days) with every interest off, i.e. stack sources only.
  • It is decided on the high end of the projection.
  • STACK_SOURCES in the script is pinned by a test to sources/source_class.rs.

4. Review fixes (round 2)

  • CriticalAlertBanner: deleted the document.hidden -> new Notification(...) effect. It was dead while document.hidden was always false; this PR would have made it live as an UNGATED native toast that duplicated the Rust critical toast. The Rust side already raises the critical toast through the interruption gate (monitoring_notifications -> notification_window::dispatch, every threshold mode). No test covered the effect.
  • Toast and brief: the page's script now ends every showing (hover pause, exit animation, then notification-hidden / briefing-hidden hides the window). The backend timer is a fallback only: toast = the script's duration + 60 s, brief = 300 s + 60 s. Before, the backend hid the toast on its own timer (4 s for watch against the script's 6 s), cutting the exit animation and ignoring hover. Tests pin the Rust durations to notification.js / briefing.js.
  • Live check (isolated debug instance, two toasts in a row: alert, then critical): the first toast hid ~6.4 s after showing (the script's 6 s + exit). After the second show, every captured frame showed either nothing or the new card; there was no frame of the previous card. The new card appeared ~190–240 ms after the command returned. The hidden toast reported visibilityState "hidden" with 0 animation frames afterwards.
  • g6DbVerdict returns pending when dbstat was unavailable: without it the non-item tables count as 0 and a small interests-off projection could pass falsely (tested).
  • webview_visibility: a show of a minimised window keeps the webview asleep (tested); comment on the one-webview-per-window subclass id.

5. Review fixes (round 3)

  • release_target also never unloads the judge when it is the user's own configured model under a non-"ollama" provider (e.g. openai-compatible pointed at the local Ollama): names compared trimmed, with an implicit :latest stripped (a_compatible_provider_on_the_same_local_model_is_never_unloaded).
  • Toast fallback hide: the +60 s slack applies only once the page has reported ready (WINDOW_READY); a page that never loaded is hidden on the script's schedule + 400 ms, so it cannot leave an empty window up for a minute (tested).
  • The Rust default toast duration now mirrors notification.js (DISMISS_DURATIONS[p] || 4000, e.g. the flood path's "high"); the include_str! pin covers the default and the exit delay.

Measurements: release build, idle, % of one core

process before (8f1012f base) after
renderer 3.76 (+0.31 second renderer) 0.55
GPU process 3.16 0.17
browser 0.00 0.02
backend 1.35 0.47
total 8.58 1.21 (bar < 2)

Measurement caveat: the "before" run had shown both a toast and the desktop brief. The "after" run showed two alert toasts (the dot-pulse case) but no brief: no morning brief fired during it, and a release build has no IPC bridge to trigger one. The brief case is covered by the debug-build causal check above, where both hidden windows were measured.

Both runs used an isolated profile: identifier com.4da.app.g6rel, FOURDA_DATA_DIR on a read-only VACUUM INTO copy of the live DB, and LLM provider none. Each was 90 s on a visible, unfocused main window between engine cycles.

pnpm run gates G6: PENDING.

  • DB size: pass. The interests-off projection is 201–204 MiB against the 500 MiB budget. The live file is still 1.65 GiB, because interest rows from before AD-054 are waiting for the last_seen prune. SQLite does not shrink the file without a VACUUM.
  • Idle CPU: still pending. gates measures idle on a running app through the bridge, and these release numbers come from the isolated instance above.
  • Cold start: still pending until it is measured on a release build.

Checks

  • tsc --noEmit clean.
  • Lint: 0 errors.
  • check-file-sizes: warnings only.
  • cargo fmt clean.
  • Clippy (round 1): the module was clean on the local toolchain. Every other error it reported is in files this PR does not touch.
  • cargo test --lib: 6262 passed, 1 failed. The failure is settings::secret_storage_tests::failed_hydration_then_save_never_deletes_or_overwrites_the_keychain, which passes 3/3 when run alone, so it looks like a parallel-run flake in shared test state. This PR does not touch that code.
  • Vitest (pre-push, round 2): 1945/1945, no unhandled errors. An earlier run had 2 unhandled "Timeout waiting for worker to respond" errors (workers for SignalsPanel.test.tsx and FirstRunTransition.test.tsx failed to start at 100% CPU).
  • Round 2: 108 scoped Rust tests pass (webview_visibility, local_judge, llm, llm_judge, notification_window, briefing_window); clippy reports nothing in the touched files on the local toolchain.

Not in this PR

  • Known follow-up (predates this PR): a click on the toast hides it without the exit animation (notification_clicked hides the window directly).
  • The Tauri npm/Rust version mismatch (@tauri-apps/api 2.11 vs tauri 2.12; updater and opener plugins likewise). A local tauri build needs --ignore-version-mismatches. It is handled separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7

runyourempire and others added 3 commits October 17, 2026 09:06
…les; DB budget

Idle CPU: Tauri's hide() and minimise hide only the native window; wry
never sets the WebView2 controller's IsVisible to false. The pre-warmed
toast and brief windows, once shown and hidden again, kept
visibilityState "visible", requestAnimationFrame at the display rate and
their infinite animations (toast dot-pulse, brief pulse-opacity) running.
New webview_visibility plugin subclasses each host window and mirrors
WM_SHOWWINDOW / minimise / restore into controller.SetIsVisible, so
every hide path (toast and brief timers, close-to-tray, minimise) stops
rendering and document.hidden now tells the truth.

Judge VRAM: keep_alive 30m -> 5m (bridges the lanes of one cycle, never
two cycles), and the cycle's last judge lane (verdict drain) unloads the
routed local judge. A user-set OLLAMA_KEEP_ALIVE or an Ollama main model
is left alone.

G6 DB budget: 500 MiB for the projected steady state at default
retention with every interest off (stack sources only), decided on the
high end of the projection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…l toast, JS-driven hides

- release_after_cycle unloads only a judge 4DA routed itself (never an
  Ollama main model), never while a judge lane of this process runs
  (LaneGuard from refresh_if_stale), never when OLLAMA_KEEP_ALIVE is set
  (reuses llm::ollama_keep_alive_for).
- A reload inside a timed judge call (another process unloaded the model
  after this lane's warm-up) is subtracted from the call's time using
  Ollama's load_duration, so it cannot trip SLOW_CALL.
- CriticalAlertBanner: delete the document.hidden -> new Notification
  effect. It was dead while document.hidden was always false and would
  now fire an ungated duplicate of the Rust critical toast, which
  already goes through the interruption gate.
- Toast and brief windows: the page's own script ends every showing
  (hover pause, exit animation, then notification-hidden /
  briefing-hidden); the backend timer is a fallback (script + 60 s).
- g6DbVerdict is pending without dbstat (non-item tables unmeasured).
- A show of a minimised window keeps its webview asleep.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
… toast fallback mirrors the page

- release_target: a non-"ollama" provider (e.g. openai-compatible) pointed
  at the local Ollama whose model is the routed judge keeps it loaded
  (names compared trimmed, implicit :latest stripped).
- Toast fallback hide: +60 s only once the page reported ready; a page
  that never loaded is hidden on the script's schedule + 400 ms.
- Rust default toast duration mirrors notification.js (|| 4000, e.g. the
  flood path's "high"); the include_str! pin covers the default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire added this pull request to the merge queue Oct 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 16, 2026
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 5d5ddcd Oct 17, 2026
25 checks passed
@runyourempire
runyourempire deleted the fix/g6-idle-cpu branch October 17, 2026 14:40
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