feat(desktop): auto-reload stale renderer when the user is away - #5588
feat(desktop): auto-reload stale renderer when the user is away#5588wpfleger96 wants to merge 2 commits into
Conversation
e671093 to
863f8a0
Compare
Long-lived renderer sessions accumulate heap and timer churn until GC pauses queue behind input, degrading scroll and click latency. The Cmd+R clean-teardown reload clears it without touching the native engine or its child agents, but relies on the user noticing and acting. Add an idle backstop that performs the same reload automatically once a session is ~12h old and the user is provably away (OS idle >= 30 min and the window unfocused). It withholds the reload while any reload-destroyed volatile work is outstanding: a starting/active huddle, a foreground or background media upload including its completion settling window, queued local file attachments a draft cannot serialize, or any in-flight send/mutation/native operation. Persisted draft text survives via localStorage, so it is deliberately not a blocker. The reload primitive is one idempotent requestRendererReload() shared by Cmd+R and the backstop: a module-level in-flight promise collapses overlapping triggers so the native WebSocket teardown never runs twice, teardown is bounded to 500 ms, and a rejecting or hung teardown is swallowed so the reload always runs -- a rejection can no longer win the race and strand the renderer on a stale heap. All fire/withhold logic is a pure, exhaustively tested predicate; the hook only wires live signals, arms after the session-age threshold, and serializes overlapping checks. Signals are read imperatively at check time (once a minute) so the hook holds no reactive subscription and never re-renders. The controller mounts inside the ready main-window shell only, guarded so Huddle companion windows and every onboarding/blocking/reset/ keyring/relaunch screen are ineligible by construction. Outstanding volatile work is tracked through one app-global registry (volatileWorkRegistry) that owns acquire/release; the native multi-file picker seam registers at the choke point via withVolatileWork so every caller is covered by construction. The deferred paperclip path is deliberately left unregistered: useFilePicker's hidden input has no cancel signal, so acquiring on open risks a hold that never releases. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
863f8a0 to
a86e5fb
Compare
…hs (#6330) Splits the `discover_acp_providers` Tauri command into a cheap cached path and an explicit forced re-discovery path. The profiling flight-recorder identified `discover_acp_providers` as the acute UI-freeze correlate: a 20–65s probe pipeline (PATH re-fetch, CLI auth probes, process spawns) was running on routine renders like channel switches, because hot surfaces remounted the wrapping query and automatic staleness refetches re-ran the full pipeline. This is complementary to the idle auto-reload (#5588) and bounded-accumulator (#5596) work. ## Backend - Added a `force` parameter to `discover_acp_providers` / `discover_acp_runtimes_from`. The default (cheap) path serves cached availability and auth statuses with zero process spawns; `force: true` runs the full pipeline (cache clear, PATH re-fetch, CLI auth probes). - Added a single-flight coalescer (`forced_single_flight`) so concurrent forced runs share one probe instead of stacking. - Extracted the auth-status cache and `resolve_auth_statuses` into `discovery/auth_status_cache.rs`. ## Renderer - Raised `staleTime` to 30 min; forced refresh flows only through an explicit `fetchQuery` path (`refreshAcpRuntimes`), so automatic staleness refetches can never re-run probes. - Added `useAcpRuntimesQueryForced` for Settings and onboarding surfaces; connect/install/save/delete mutations force-refresh in `onSettled`. - Switched `SetupStep`, `HarnessCatalogDialog`, and `HarnessesSettingsPanel` to the forced hook. ## Structure The touched files were at or over the grandfathered 1000-line file-size ceiling, so the change is delivered as cohesive extractions rather than in-place growth: `forced_single_flight.rs`, `auth_status_cache.rs`, `discovery/tests/forced_discovery.rs`, `acpRuntimesQuery.ts`, and `tauriAcpDiscovery.ts`. Dependencies are one-directional (`hooks.ts` imports from `acpRuntimesQuery.ts` and re-exports the forced hook, so consumer imports are unchanged; no import cycle). ## Tests Added Rust tests pinning the cheap-vs-forced split: a probe is spawned only when forced, the cheap path reuses the cache, and auth status is `Unknown` before any probe runs. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…hs (block#6330) Splits the `discover_acp_providers` Tauri command into a cheap cached path and an explicit forced re-discovery path. The profiling flight-recorder identified `discover_acp_providers` as the acute UI-freeze correlate: a 20–65s probe pipeline (PATH re-fetch, CLI auth probes, process spawns) was running on routine renders like channel switches, because hot surfaces remounted the wrapping query and automatic staleness refetches re-ran the full pipeline. This is complementary to the idle auto-reload (block#5588) and bounded-accumulator (block#5596) work. ## Backend - Added a `force` parameter to `discover_acp_providers` / `discover_acp_runtimes_from`. The default (cheap) path serves cached availability and auth statuses with zero process spawns; `force: true` runs the full pipeline (cache clear, PATH re-fetch, CLI auth probes). - Added a single-flight coalescer (`forced_single_flight`) so concurrent forced runs share one probe instead of stacking. - Extracted the auth-status cache and `resolve_auth_statuses` into `discovery/auth_status_cache.rs`. ## Renderer - Raised `staleTime` to 30 min; forced refresh flows only through an explicit `fetchQuery` path (`refreshAcpRuntimes`), so automatic staleness refetches can never re-run probes. - Added `useAcpRuntimesQueryForced` for Settings and onboarding surfaces; connect/install/save/delete mutations force-refresh in `onSettled`. - Switched `SetupStep`, `HarnessCatalogDialog`, and `HarnessesSettingsPanel` to the forced hook. ## Structure The touched files were at or over the grandfathered 1000-line file-size ceiling, so the change is delivered as cohesive extractions rather than in-place growth: `forced_single_flight.rs`, `auth_status_cache.rs`, `discovery/tests/forced_discovery.rs`, `acpRuntimesQuery.ts`, and `tauriAcpDiscovery.ts`. Dependencies are one-directional (`hooks.ts` imports from `acpRuntimesQuery.ts` and re-exports the forced hook, so consumer imports are unchanged; no import cycle). ## Tests Added Rust tests pinning the cheap-vs-forced split: a probe is spawned only when forced, the cheap path reuses the cache, and auth status is `Unknown` before any probe runs. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
BLOCKING: REQUEST CHANGES
Reviewed exact head ac3c3d6c62897f643b65f450efbecf10571ca948 against base bbd20fae75ecc3bd7a83cc12a65379fac22a2b79 by source inspection only.
[P1] The volatile-work closed set still permits idle reload to erase unsent drafts
The feature contract says a reload fires only when no reload-destroyed work exists, but two ready-shell surfaces retain unsent state solely in renderer memory without registering it:
- Completed forum attachments.
ForumComposerregisters only nonblank text (desktop/src/features/forum/ui/ForumComposer.tsx:59-64), whileuseMediaUploadregisters only active uploads or queued local files (desktop/src/features/messages/lib/useMediaUpload.ts:901-916). After an immediate upload completes, an attachment-only forum post/reply/note/issue/PR-comment remains inpendingImeta, but every reload blocker is false. On an old session, leaving the app unfocused for the idle threshold reloads and discards that draft. - Send Feedback drafts. The dialog’s message/category/log selection (
desktop/src/features/settings/ui/SendFeedbackDialog.tsx:91-107) and completed attachment descriptor (desktop/src/features/settings/hooks/useSendFeedback.ts:49-71) are neither persisted nor registered.queryClient.isMutating()protects submission, and the picker wrapper protects only the picker/upload operation; once either settles, an open unsent feedback draft can be erased by the same idle reload.
Please make every non-persisted draft state hold the volatile-work registry until it is submitted, explicitly discarded, or unmounted/closed, and add lifecycle tests for completed attachment-only forum drafts and populated Send Feedback drafts. The existing wiring tests cover in-flight/deferred uploads but do not cover these settled in-memory states.
No PR code was checked out or executed.
Summary
Long-lived Buzz desktop renderer sessions accumulate heap and timer churn until GC pauses queue behind input, degrading scroll and click latency (the multi-GB, multi-hour sessions that motivate this). The
Cmd+Rclean-teardown reload clears it without touching the native engine or its child agents — but relies on the user noticing the degradation and acting.This adds an idle backstop that performs the same clean-teardown reload automatically, but only when the user is provably away and the session is old enough to have accumulated the churn — never during use.
How it decides
The backstop arms once the renderer session is
~12hold, then checks once a minute. It reloads only when all of these hold:>= 12h>= 30 min(unknown/unsupported idle API → never fire)Volatile-work registry
Everything a reload would destroy is tracked through one app-global registry (
volatileWorkRegistry.ts) that ownsacquire/releaseand exposes a singlehasVolatileWork()the policy reads. The registry is the sole blocker surface, covering:isCompleting/onCompletesettling windowFileattachments a draft cannot serializequeryClient.isMutating()— sends, install/import/save all run as mutations)The native picker registers at the choke point via one
withVolatileWork(label, run)helper intauriMedia.ts, so every caller ofpickAndUploadMediais covered by construction and the acquire/release is balanced on both resolve and reject — no caller can forget to register, and no path can leak a key.pickAndUploadMedialives beside its siblingpickAndUploadImageintauriMedia.tsand is re-exported fromtauri.ts, so existing@/shared/api/tauriimports are unchanged.Draft text survives a reload — persisted drafts sit in
localStorage, and text typed since the last persist is flushed synchronously onpagehide(see below) — so draft text is deliberately not a blocker.Why the paperclip's own dialog is left unregistered
The deferred paperclip path (
handlePaperclipwhenqueueUntilSend) opensuseFilePicker's hidden<input type="file">rather than the native picker, and it is deliberately not registered with the registry:changefires.useFilePicker's own comment records that "Cancel emits nochange" — cancelling the dialog discards everything, and achangeroutes straight intoqueueFiles/uploadFiles, which are already covered by the registeredqueuedAttachments/uploadingCounthold.Reload primitive
Cmd+Rand the backstop now share one idempotentrequestRendererReload():Cmd+R, or the backstop firing mid-teardown) so the native WebSocket teardown never runs twice.RELOAD_TEARDOWN_TIMEOUT_MS(500 ms); a hung socket loses to the timeout.finally— a rejection can no longer win thePromise.raceand strand the renderer on a stale heap (the pre-existingCmd+Rbug this replaces).Composer flush on reload
Live composer editor text was persisted to
localStorageonly on the draft-key-change effect cleanup inuseDraftPersistSnapshot.ts, andlocation.reload()does not run React effect cleanup — so text typed-but-not-yet-persisted would be lost on any reload (backstop,Cmd+R, or window close).useDraftPersistLifecyclenow registers apagehidelistener that flushes the current draft synchronously, mirroring the precedent inuseObservedUnreadPersistence.ts. The persist logic is factored into onepersistDraftSnapshot(key, channelId)helper so the key-change cleanup and thepagehideflush share a single write path — the cleanup persists the outgoing draft, the flush persists the current one, from the same live editor content. This closes the reload-destroyed-text gap for all three reload paths in one mechanism.Structure
idleAutoReloadPolicy.ts); the hook (useIdleAutoReload.ts) only wires live signals, arms after the session-age threshold, and serializes overlapping checks.readymain-window shell only, guarded byhuddleWindowChannelId() === null— so Huddle companion windows and every onboarding/blocking/reset/keyring/relaunch screen are ineligible by construction, andisMutating()reads the message-send query client.