mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): platforms.changed broadcast retires the Messaging page's 6s status poll
The change watcher (#73673) missed one always-on-while-mounted timer: the Messaging page polled /api/messaging/platforms every 6s for connection status. The gateway already persists platform connect/disconnect/health to gateway_state.json, so watch that file's mtime and broadcast platforms.changed (floored to 5s — the gateway also rewrites the file for in-flight-count bookkeeping), route it through live-sync like its siblings, and refresh the page on the tick. Older backends keep the legacy visible-tab poll verbatim. Finishes the always-on poll sweep for #73618.
This commit is contained in:
parent
7142dc4580
commit
f8e07a332e
5 changed files with 62 additions and 8 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { useStore } from '@nanostores/react'
|
||||
import type * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ import { openExternalLink } from '@/lib/external-link'
|
|||
import { ExternalLink, Save, Trash2 } from '@/lib/icons'
|
||||
import { normalize } from '@/lib/text'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $changeEventsAvailable, $platformsChangeTick } from '@/store/live-sync'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { runGatewayRestart } from '@/store/system-actions'
|
||||
|
||||
|
|
@ -141,9 +143,26 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
|
|||
void refreshPlatforms()
|
||||
}, [refreshPlatforms])
|
||||
|
||||
// Auto-poll while the user is on the messaging page so connection status
|
||||
// updates without a manual "check" click. Pause when the tab is hidden.
|
||||
const changeEventsAvailable = useStore($changeEventsAvailable)
|
||||
const platformsChangeTick = useStore($platformsChangeTick)
|
||||
|
||||
// Connection status updates without a manual "check" click. platforms.changed
|
||||
// (the gateway persisting connect/disconnect/health to gateway_state.json)
|
||||
// drives the refresh on event-capable backends — no timer; older backends
|
||||
// keep the legacy visible-tab poll.
|
||||
useEffect(() => {
|
||||
if (!changeEventsAvailable || platformsChangeTick === 0 || document.hidden) {
|
||||
return
|
||||
}
|
||||
|
||||
void refreshPlatforms(true)
|
||||
}, [changeEventsAvailable, platformsChangeTick, refreshPlatforms])
|
||||
|
||||
useEffect(() => {
|
||||
if (changeEventsAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
function tick() {
|
||||
|
|
@ -160,7 +179,7 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, .
|
|||
cancelled = true
|
||||
window.clearInterval(id)
|
||||
}
|
||||
}, [refreshPlatforms])
|
||||
}, [changeEventsAvailable, refreshPlatforms])
|
||||
|
||||
const selected = useMemo(() => {
|
||||
if (!platforms) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { applyGoalStatusText } from '@/store/goals'
|
|||
import {
|
||||
notifyCronChanged,
|
||||
notifyPetChanged,
|
||||
notifyPlatformsChanged,
|
||||
notifySessionsChanged,
|
||||
type PetChangeMeta,
|
||||
setChangeEventsAvailable
|
||||
|
|
@ -298,7 +299,12 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
}
|
||||
|
||||
return
|
||||
} else if (event.type === 'pet.changed' || event.type === 'cron.changed' || event.type === 'sessions.changed') {
|
||||
} else if (
|
||||
event.type === 'pet.changed' ||
|
||||
event.type === 'cron.changed' ||
|
||||
event.type === 'sessions.changed' ||
|
||||
event.type === 'platforms.changed'
|
||||
) {
|
||||
// Change-watcher broadcasts (server._broadcast_watched_changes): the
|
||||
// backend's on-disk signature moved. Route to the live-sync ticks the
|
||||
// former pollers now subscribe to. Only the active profile's changes
|
||||
|
|
@ -311,6 +317,8 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
notifyPetChanged(payload as PetChangeMeta | undefined)
|
||||
} else if (event.type === 'cron.changed') {
|
||||
notifyCronChanged()
|
||||
} else if (event.type === 'platforms.changed') {
|
||||
notifyPlatformsChanged()
|
||||
} else {
|
||||
notifySessionsChanged()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const $changeEventsAvailable = atom(false)
|
|||
|
||||
export const $cronChangeTick = atom(0)
|
||||
export const $sessionsChangeTick = atom(0)
|
||||
export const $platformsChangeTick = atom(0)
|
||||
|
||||
/** `pet.info.meta`-shaped payload carried on `pet.changed` — lets the pet skip
|
||||
* the heavy sprite refetch when the broadcast already says enabled=false. */
|
||||
|
|
@ -47,6 +48,10 @@ export function notifySessionsChanged(): void {
|
|||
$sessionsChangeTick.set($sessionsChangeTick.get() + 1)
|
||||
}
|
||||
|
||||
export function notifyPlatformsChanged(): void {
|
||||
$platformsChangeTick.set($platformsChangeTick.get() + 1)
|
||||
}
|
||||
|
||||
/** Reset on gateway wipe/reconnect — a new backend re-advertises capability on
|
||||
* its own gateway.ready, and stale ticks must not fire refreshes into stores
|
||||
* the wipe just cleared. */
|
||||
|
|
|
|||
|
|
@ -62,6 +62,16 @@ def test_state_db_move_broadcasts_sessions_changed(watcher_home):
|
|||
assert ("sessions.changed", {}) in events
|
||||
|
||||
|
||||
def test_gateway_state_move_broadcasts_platforms_changed(watcher_home):
|
||||
home, events = watcher_home
|
||||
server._broadcast_watched_changes(now=0.0)
|
||||
|
||||
(home / "gateway_state.json").write_text('{"platforms": {}}')
|
||||
server._broadcast_watched_changes(now=10.0)
|
||||
|
||||
assert ("platforms.changed", {}) in events
|
||||
|
||||
|
||||
def test_sessions_floor_coalesces_burst_but_keeps_trailing_edge(watcher_home):
|
||||
home, events = watcher_home
|
||||
server._broadcast_watched_changes(now=0.0)
|
||||
|
|
|
|||
|
|
@ -3072,6 +3072,16 @@ def _sessions_sig():
|
|||
return sig
|
||||
|
||||
|
||||
def _platforms_sig():
|
||||
"""mtime of gateway_state.json — the messaging gateway process persists
|
||||
platform connect/disconnect/health there, so its movement is the
|
||||
"connection status changed" signal for the Messaging page."""
|
||||
try:
|
||||
return (_watcher_home() / "gateway_state.json").stat().st_mtime_ns
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
# Watched change signals: event → (check interval, signature fn, payload fn).
|
||||
# Signatures are stat/dict-lookup cheap, same bar as the skin watcher; the
|
||||
# check interval keeps the pricier probes (pet resolves the active sheet off
|
||||
|
|
@ -3080,12 +3090,14 @@ _CHANGE_WATCHES: dict[str, tuple[float, Any, Any]] = {
|
|||
"pet.changed": (2.0, _pet_sig, _pet_changed_payload),
|
||||
"cron.changed": (1.0, _cron_sig, lambda: {}),
|
||||
"sessions.changed": (0.5, _sessions_sig, lambda: {}),
|
||||
"platforms.changed": (2.0, _platforms_sig, lambda: {}),
|
||||
}
|
||||
|
||||
# state.db moves on every message append during a streaming turn; the floor
|
||||
# coalesces that burst to one broadcast per window (trailing edge included —
|
||||
# a floored change keeps its old signature and re-fires next tick).
|
||||
_CHANGE_BROADCAST_FLOOR_S = {"sessions.changed": 2.0}
|
||||
# state.db moves on every message append during a streaming turn, and the
|
||||
# gateway rewrites gateway_state.json for in-flight-count bookkeeping; the
|
||||
# floor coalesces those bursts to one broadcast per window (trailing edge
|
||||
# included — a floored change keeps its old signature and re-fires next tick).
|
||||
_CHANGE_BROADCAST_FLOOR_S = {"sessions.changed": 2.0, "platforms.changed": 5.0}
|
||||
|
||||
_change_sigs: dict[str, Any] = {}
|
||||
_change_checked_at: dict[str, float] = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue