mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
feat(chat): persist attach token, reconnect on transient close
ChatPage sends ?attach=<localStorage token> so /chat reattaches to its live PTY across refresh. onclose: 4410=process-exit (session ended), 4409=superseded (quiet), else transient -> auto-reconnect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c3d2be073a
commit
79f4f78fa4
1 changed files with 55 additions and 0 deletions
|
|
@ -40,6 +40,34 @@ import { PluginSlot } from "@/plugins";
|
|||
import { useTheme } from "@/themes";
|
||||
import { useProfileScope } from "@/contexts/useProfileScope";
|
||||
|
||||
// Stable per-browser token identifying THIS chat tab's keep-alive PTY session.
|
||||
// Sent as ?attach=; lets a refresh/disconnect reattach to the same live process
|
||||
// instead of spawning a fresh one. Per-localStorage, so other devices can't grab it.
|
||||
// ``rotate`` mints a new token — used when the user explicitly starts a fresh
|
||||
// session so the old keep-alive PTY is NOT reattached (the registry reaps it).
|
||||
const PTY_ATTACH_TOKEN_KEY = "hermes.pty.token.chat";
|
||||
function ptyAttachToken(rotate = false): string {
|
||||
let t = "";
|
||||
if (!rotate) {
|
||||
try {
|
||||
t = window.localStorage.getItem(PTY_ATTACH_TOKEN_KEY) ?? "";
|
||||
} catch {
|
||||
/* private mode / storage blocked */
|
||||
}
|
||||
}
|
||||
if (!t) {
|
||||
const a = new Uint8Array(16);
|
||||
crypto.getRandomValues(a);
|
||||
t = Array.from(a, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
try {
|
||||
window.localStorage.setItem(PTY_ATTACH_TOKEN_KEY, t);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// Channel id ties this chat tab's PTY child (publisher) to its sidebar
|
||||
// (subscriber). Generated once per mount so a tab refresh starts a fresh
|
||||
// channel — the previous PTY child terminates with the old WS, and its
|
||||
|
|
@ -679,6 +707,10 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
|
|||
const params: Record<string, string> = { channel };
|
||||
if (resumeParam) params.resume = resumeParam;
|
||||
if (forceFresh) params.fresh = "1";
|
||||
// Keep-alive identity: reattach to this tab's living PTY across
|
||||
// refresh/transient drops. A forced-fresh start rotates the token so
|
||||
// the previous keep-alive PTY is not reattached (registry reaps it).
|
||||
params.attach = ptyAttachToken(forceFresh);
|
||||
// Profile-scoped chat: the PTY child gets HERMES_HOME pointed at the
|
||||
// selected profile, so the conversation runs with that profile's model,
|
||||
// skills, memory, and sessions (see web_server._resolve_chat_argv).
|
||||
|
|
@ -693,6 +725,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
|
|||
reconnectAttemptRef.current = 0;
|
||||
setBanner(null);
|
||||
setSessionEnded(false);
|
||||
// Connected — cancel any pending reconnect from a prior transient drop.
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
reconnectTimerRef.current = null;
|
||||
}
|
||||
// Send the initial RESIZE immediately so Ink has *a* size to lay
|
||||
// out against on its first paint. The double-rAF block above will
|
||||
// follow up with the authoritative measurement — at worst Ink
|
||||
|
|
@ -775,7 +812,21 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
|
|||
// Server already wrote an ANSI error frame.
|
||||
return;
|
||||
}
|
||||
// Keep-alive close-code contract (web_server.pty_ws + pty_session):
|
||||
// 4410 = the agent PROCESS exited (real end) → restart affordance.
|
||||
// 4409 = superseded by a newer tab attaching the same token → stay quiet.
|
||||
if (ev.code === 4410) {
|
||||
term.write(`\r\n\x1b[90m[session ended]\x1b[0m\r\n`);
|
||||
setSessionEnded(true);
|
||||
return;
|
||||
}
|
||||
if (ev.code === 4409) {
|
||||
return;
|
||||
}
|
||||
if (!ev.wasClean || ev.code === 1001 || ev.code === 1006) {
|
||||
// Transient transport drop (refresh, sleep/wake, signal loss).
|
||||
// Reconnect with backoff; the same ?attach= token reattaches to
|
||||
// the still-living PTY, so the conversation continues in place.
|
||||
scheduleReconnect(ev.code);
|
||||
return;
|
||||
}
|
||||
|
|
@ -854,6 +905,10 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
|
|||
clearTimeout(copyResetRef.current);
|
||||
copyResetRef.current = null;
|
||||
}
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
reconnectTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [channel, clearReconnectTimer, resumeParam, scopedProfile, reconnectNonce]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue