mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
perf(desktop): faster session resume & warm AudioContext at idle
- Resume: fire the REST transcript prefetch and the session.resume RPC in parallel, and skip the redundant message conversion + reconciliation when the prefetch already hydrated the transcript. - Haptics: web-haptics builds its AudioContext lazily on first trigger, paying the ~850ms CoreAudio spin-up on the first streamStart haptic as the first token paints. Open/close a throwaway context at idle so the real one connects to an already-warm audio service.
This commit is contained in:
parent
edc36f3a45
commit
3cf7d43262
2 changed files with 57 additions and 17 deletions
|
|
@ -618,10 +618,26 @@ export function useSessionActions({
|
|||
const watchWindow = isWatchWindow()
|
||||
let localSnapshot = $messages.get()
|
||||
|
||||
// REST transcript prefetch and the gateway resume RPC are independent
|
||||
// — run them concurrently so a big session's wall time is
|
||||
// max(prefetch, resume) instead of their sum. The prefetch paints the
|
||||
// transcript as soon as it lands; the RPC binds the runtime id.
|
||||
// Watch windows skip the prefetch — lazy resume attaches the live mirror.
|
||||
const prefetchPromise = watchWindow ? null : getSessionMessages(storedSessionId, sessionProfile)
|
||||
|
||||
const resumePromise = requestGateway<SessionResumeResponse>('session.resume', {
|
||||
session_id: storedSessionId,
|
||||
cols: 96,
|
||||
...(watchWindow ? { lazy: true } : {}),
|
||||
...(sessionProfile ? { profile: sessionProfile } : {})
|
||||
})
|
||||
// The rejection is consumed by the `await` below; this guard only
|
||||
// keeps it from surfacing as unhandled while the prefetch settles.
|
||||
resumePromise.catch(() => undefined)
|
||||
|
||||
try {
|
||||
// Watch windows skip REST prefetch — lazy resume attaches the live mirror.
|
||||
if (!watchWindow) {
|
||||
const storedMessages = await getSessionMessages(storedSessionId, sessionProfile)
|
||||
if (prefetchPromise) {
|
||||
const storedMessages = await prefetchPromise
|
||||
|
||||
if (isCurrentResume()) {
|
||||
localSnapshot = preserveLocalAssistantErrors(toChatMessages(storedMessages.messages), $messages.get())
|
||||
|
|
@ -635,12 +651,7 @@ export function useSessionActions({
|
|||
// Non-fatal: gateway resume below can still hydrate the session.
|
||||
}
|
||||
|
||||
const resumed = await requestGateway<SessionResumeResponse>('session.resume', {
|
||||
session_id: storedSessionId,
|
||||
cols: 96,
|
||||
...(watchWindow ? { lazy: true } : {}),
|
||||
...(sessionProfile ? { profile: sessionProfile } : {})
|
||||
})
|
||||
const resumed = await resumePromise
|
||||
|
||||
if (!isCurrentResume()) {
|
||||
return
|
||||
|
|
@ -648,17 +659,22 @@ export function useSessionActions({
|
|||
|
||||
const currentMessages = $messages.get()
|
||||
|
||||
const resumedMessages = preserveLocalAssistantErrors(
|
||||
reconcileResumeMessages(toChatMessages(resumed.messages), currentMessages),
|
||||
currentMessages
|
||||
)
|
||||
// Keep the local snapshot when resume would only reshuffle runtime projection.
|
||||
// Keep the local snapshot when resume would only reshuffle runtime
|
||||
// projection. When the REST prefetch already hydrated the transcript,
|
||||
// skip converting/reconciling the resume payload entirely — on a
|
||||
// 1000+-message session that second conversion plus the deep
|
||||
// equivalence compare costs over a second of main-thread time.
|
||||
const preferredMessages =
|
||||
localSnapshot.length > 0
|
||||
? localSnapshot
|
||||
: chatMessageArraysEquivalent(currentMessages, resumedMessages)
|
||||
? currentMessages
|
||||
: resumedMessages
|
||||
: (() => {
|
||||
const resumedMessages = preserveLocalAssistantErrors(
|
||||
reconcileResumeMessages(toChatMessages(resumed.messages), currentMessages),
|
||||
currentMessages
|
||||
)
|
||||
|
||||
return chatMessageArraysEquivalent(currentMessages, resumedMessages) ? currentMessages : resumedMessages
|
||||
})()
|
||||
|
||||
const messagesForView = preserveLocalAssistantErrors(preferredMessages, currentMessages)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,5 +15,29 @@ export function HapticsProvider({ children }: { children: ReactNode }) {
|
|||
return () => registerHapticTrigger(null)
|
||||
}, [muted, trigger])
|
||||
|
||||
// web-haptics builds its AudioContext lazily inside the first trigger(), and
|
||||
// the process's first AudioContext pays the CoreAudio spin-up (~850ms stall
|
||||
// in profiles) — which landed on the first streamStart haptic as the first
|
||||
// token painted. Open/close a throwaway context at idle so the real one
|
||||
// connects to an already-warm audio service in single-digit ms.
|
||||
useEffect(() => {
|
||||
if (typeof requestIdleCallback !== 'function' || typeof AudioContext === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const id = requestIdleCallback(
|
||||
() => {
|
||||
try {
|
||||
void new AudioContext().close().catch(() => undefined)
|
||||
} catch {
|
||||
// No audio device (headless CI) — nothing to warm.
|
||||
}
|
||||
},
|
||||
{ timeout: 2000 }
|
||||
)
|
||||
|
||||
return () => cancelIdleCallback(id)
|
||||
}, [])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue