mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
Merge pull request #44047 from kshitijk4poor/salvage/desktop-stop-stale-session
fix(desktop): recover stale session before stop
This commit is contained in:
commit
4829f8d2c5
2 changed files with 81 additions and 8 deletions
|
|
@ -42,6 +42,7 @@ function sessionInfo(overrides: Partial<SessionInfo> = {}): SessionInfo {
|
|||
}
|
||||
|
||||
interface HarnessHandle {
|
||||
cancelRun: () => Promise<void>
|
||||
steerPrompt: (text: string) => Promise<boolean>
|
||||
submitText: (
|
||||
text: string,
|
||||
|
|
@ -102,8 +103,12 @@ function Harness({
|
|||
})
|
||||
|
||||
useEffect(() => {
|
||||
onReady({ steerPrompt: actions.steerPrompt, submitText: actions.submitText })
|
||||
}, [actions.steerPrompt, actions.submitText, onReady])
|
||||
onReady({
|
||||
cancelRun: actions.cancelRun,
|
||||
steerPrompt: actions.steerPrompt,
|
||||
submitText: actions.submitText
|
||||
})
|
||||
}, [actions.cancelRun, actions.steerPrompt, actions.submitText, onReady])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -629,6 +634,43 @@ describe('usePromptActions sleep/wake session recovery', () => {
|
|||
expect(calls[2]?.params).toEqual({ session_id: RECOVERED_SESSION_ID, text: 'message after wake' })
|
||||
})
|
||||
|
||||
it('resumes the stored session and retries once when session.interrupt reports "session not found"', async () => {
|
||||
const calls: { method: string; params?: Record<string, unknown> }[] = []
|
||||
let interruptAttempts = 0
|
||||
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
|
||||
calls.push({ method, params })
|
||||
if (method === 'session.interrupt') {
|
||||
interruptAttempts += 1
|
||||
if (interruptAttempts === 1) {
|
||||
throw new Error('session not found')
|
||||
}
|
||||
return {} as never
|
||||
}
|
||||
if (method === 'session.resume') {
|
||||
return { session_id: RECOVERED_SESSION_ID } as never
|
||||
}
|
||||
return {} as never
|
||||
})
|
||||
|
||||
let handle: HarnessHandle | null = null
|
||||
render(
|
||||
<Harness
|
||||
onReady={h => (handle = h)}
|
||||
refreshSessions={async () => undefined}
|
||||
requestGateway={requestGateway}
|
||||
storedSessionId={STORED_SESSION_ID}
|
||||
/>
|
||||
)
|
||||
await waitFor(() => expect(handle).not.toBeNull())
|
||||
|
||||
await handle!.cancelRun()
|
||||
|
||||
expect(calls.map(c => c.method)).toEqual(['session.interrupt', 'session.resume', 'session.interrupt'])
|
||||
expect(calls[0]?.params).toEqual({ session_id: RUNTIME_SESSION_ID })
|
||||
expect(calls[1]?.params).toEqual({ session_id: STORED_SESSION_ID })
|
||||
expect(calls[2]?.params).toEqual({ session_id: RECOVERED_SESSION_ID })
|
||||
})
|
||||
|
||||
it('surfaces the original error (no resume) when the failure is not "session not found"', async () => {
|
||||
const calls: string[] = []
|
||||
const states: Record<string, unknown>[] = []
|
||||
|
|
@ -818,4 +860,3 @@ describe('uploadComposerAttachment remote read failures', () => {
|
|||
).rejects.toThrow('ENOENT: no such file')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,12 @@ function inlineErrorMessage(error: unknown, fallback: string): string {
|
|||
return (raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw).replace(/^Error:\s*/, '').trim()
|
||||
}
|
||||
|
||||
function isSessionNotFoundError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
return /session not found/i.test(message)
|
||||
}
|
||||
|
||||
function base64FromDataUrl(dataUrl: string): string {
|
||||
const comma = dataUrl.indexOf(',')
|
||||
|
||||
|
|
@ -661,9 +667,7 @@ export function usePromptActions({
|
|||
try {
|
||||
await requestGateway('prompt.submit', { session_id: sessionId, text })
|
||||
} catch (firstErr) {
|
||||
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr)
|
||||
|
||||
if (/session not found/i.test(firstMsg) && selectedStoredSessionIdRef.current) {
|
||||
if (isSessionNotFoundError(firstErr) && selectedStoredSessionIdRef.current) {
|
||||
// Re-register the session in the gateway and get a fresh live ID.
|
||||
const resumed = await requestGateway<{ session_id: string }>('session.resume', {
|
||||
session_id: selectedStoredSessionIdRef.current
|
||||
|
|
@ -1273,11 +1277,39 @@ export function usePromptActions({
|
|||
try {
|
||||
await requestGateway('session.interrupt', { session_id: sessionId })
|
||||
} catch (err) {
|
||||
let stopError = err
|
||||
|
||||
if (isSessionNotFoundError(err) && selectedStoredSessionIdRef.current) {
|
||||
try {
|
||||
const resumed = await requestGateway<{ session_id: string }>('session.resume', {
|
||||
session_id: selectedStoredSessionIdRef.current
|
||||
})
|
||||
const recoveredId = resumed?.session_id
|
||||
|
||||
if (recoveredId) {
|
||||
activeSessionIdRef.current = recoveredId
|
||||
await requestGateway('session.interrupt', { session_id: recoveredId })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (resumeErr) {
|
||||
stopError = resumeErr
|
||||
}
|
||||
}
|
||||
|
||||
setMutableRef(busyRef, false)
|
||||
setBusy(false)
|
||||
notifyError(err, copy.stopFailed)
|
||||
notifyError(stopError, copy.stopFailed)
|
||||
}
|
||||
}, [activeSessionId, activeSessionIdRef, busyRef, copy.stopFailed, requestGateway, updateSessionState])
|
||||
}, [
|
||||
activeSessionId,
|
||||
activeSessionIdRef,
|
||||
busyRef,
|
||||
copy.stopFailed,
|
||||
requestGateway,
|
||||
selectedStoredSessionIdRef,
|
||||
updateSessionState
|
||||
])
|
||||
|
||||
// Steer = nudge the live turn without interrupting: the gateway appends the
|
||||
// text to the next tool result so the model reads it on its next iteration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue