mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
The 8-minute stream-silence watchdog only removed a stuck session from $workingSessionIds (the sidebar dot). The composer's busy state lives in the session-state cache and was never cleared, so a hung or looping turn that never delivered its terminal event — including an old session re-opened while the backend still reports it "running" — stayed wedged on "Thinking" / Stop indefinitely. Have the watchdog notify subscribers when it force-clears a session, and subscribe from the session-state cache to also drop that session's busy/awaiting/needsInput flags. updateSessionState re-syncs $busy when the healed session is the one on screen, so the composer recovers instead of spinning forever. Frontend-only safety net; doesn't touch the turn lifecycle. The backend root (a stale in-memory session["running"] surviving a dead turn thread and re-arming busy on every resume) is a separate follow-up.
59 lines
1.6 KiB
TypeScript
59 lines
1.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { $workingSessionIds, onSessionWatchdogClear, setSessionWorking, setWorkingSessionIds } from './session'
|
|
|
|
const WATCHDOG_MS = 8 * 60 * 1000
|
|
|
|
describe('session watchdog', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
setWorkingSessionIds(() => [])
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.runOnlyPendingTimers()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('drops a stuck session and notifies listeners once the silence window elapses', () => {
|
|
const cleared: string[] = []
|
|
const off = onSessionWatchdogClear(id => cleared.push(id))
|
|
|
|
setSessionWorking('s1', true)
|
|
expect($workingSessionIds.get()).toContain('s1')
|
|
|
|
vi.advanceTimersByTime(WATCHDOG_MS)
|
|
|
|
// Both the sidebar dot AND the busy-clearing signal fire — the contract
|
|
// that lets the composer recover from a hung/looping turn, not just the dot.
|
|
expect($workingSessionIds.get()).not.toContain('s1')
|
|
expect(cleared).toEqual(['s1'])
|
|
|
|
off()
|
|
})
|
|
|
|
it('never fires for a session that settles before the window', () => {
|
|
const cleared: string[] = []
|
|
const off = onSessionWatchdogClear(id => cleared.push(id))
|
|
|
|
setSessionWorking('s2', true)
|
|
setSessionWorking('s2', false)
|
|
|
|
vi.advanceTimersByTime(WATCHDOG_MS)
|
|
|
|
expect(cleared).toEqual([])
|
|
|
|
off()
|
|
})
|
|
|
|
it('stops notifying after unsubscribe', () => {
|
|
const cleared: string[] = []
|
|
const off = onSessionWatchdogClear(id => cleared.push(id))
|
|
off()
|
|
|
|
setSessionWorking('s3', true)
|
|
vi.advanceTimersByTime(WATCHDOG_MS)
|
|
|
|
expect(cleared).toEqual([])
|
|
})
|
|
})
|