package main // The chat router proxies browser traffic to the tenant Hermes WebUI, whose // session read contract is `GET /api/session?session_id=`. The dashboard // style `/api/sessions/[/messages]` routes belong to the separate Hermes // agent deployment (services/hermes/scripts/patch_web_session_activity.py) and // are unrouted here, so polling them returned the WebUI's generic 404 on every // attempt and rendered a false "unavailable to this account" banner after every // full page load — exactly what a Keycloak logout/login round-trip produces. const sessionFallbackPath = "/api/session" const sessionContinuityJS = `(() => { const match = location.pathname.match(/^\/session\/([^/]+)\/?$/); if (!match) return; let sessionId; try { sessionId = decodeURIComponent(match[1]); } catch (_) { sessionId = match[1]; } const started = Date.now(); let timer = 0; let request = null; const card = document.createElement('aside'); card.id = 'hermes-session-continuity'; card.setAttribute('role', 'status'); card.setAttribute('aria-live', 'polite'); card.setAttribute('aria-atomic', 'true'); card.hidden = true; card.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:1000;max-width:min(420px,calc(100vw - 32px));box-sizing:border-box;padding:10px 12px;border:1px solid #475569;border-radius:10px;background:#111827;color:#e5e7eb;font:13px/1.4 system-ui,sans-serif;box-shadow:0 8px 28px #0008'; const message = document.createElement('span'); const newChat = document.createElement('a'); newChat.href = '/'; newChat.textContent = ' Start a new chat.'; newChat.style.color = '#7dd3fc'; card.append(message, newChat); document.body.appendChild(card); const show = (text, busy, link) => { message.textContent = text; card.hidden = false; card.setAttribute('aria-busy', busy ? 'true' : 'false'); newChat.hidden = !link; }; const hide = () => { card.hidden = true; card.setAttribute('aria-busy', 'false'); }; const activityLabel = (messages) => { const latest = messages[messages.length - 1] || {}; if (latest.tool_name) return 'tool ' + String(latest.tool_name).slice(0, 80); if (Array.isArray(latest.tool_calls) && latest.tool_calls.length) { const call = latest.tool_calls[latest.tool_calls.length - 1] || {}; return 'tool ' + String((call.function || {}).name || 'activity').slice(0, 80); } if (latest.activity_event) return String(latest.activity_event).replace(/[._-]+/g, ' ').slice(0, 80); if (latest.observed && typeof latest.content === 'string') return latest.content.slice(0, 120); return latest.role === 'assistant' ? 'assistant update' : latest.role === 'user' ? 'request stored' : 'working'; }; // Only the WebUI's own answers decide what the banner claims. A 409 is the // single case where the stored session really is out of this account's // active scope; a 404 means the conversation is no longer stored at all. const handled = async (response) => { if (response.status === 401 || response.status === 403) { const rd = location.pathname + location.search + location.hash; location.assign('/oauth2/start?rd=' + encodeURIComponent(rd)); return 'auth'; } if (response.status === 409) { let payload = {}; try { payload = await response.json(); } catch (_) { payload = {}; } show(payload.code === 'session_profile_mismatch' ? 'This session belongs to a different profile on this account. Switch profiles to reopen it.' : 'This session is unavailable to this account.', false, true); schedule(15000); return 'scoped'; } if (response.status === 404) { show('This conversation is no longer stored in your private chat.', false, true); schedule(15000); return 'missing'; } return ''; }; const schedule = (delay) => { clearTimeout(timer); timer = window.setTimeout(poll, delay); }; async function poll() { if (request) return; request = new AbortController(); const timeout = window.setTimeout(() => request && request.abort(), 8000); try { const query = '/api/session?session_id=' + encodeURIComponent(sessionId) + '&messages=1&msg_limit=24&resolve_model=0&hermes_fallback=1'; const response = await fetch(query, {cache:'no-store', credentials:'same-origin', signal:request.signal}); if (await handled(response)) return; if (!response.ok) throw new Error('session poll failed'); const payload = await response.json(); const session = payload && typeof payload.session === 'object' && payload.session ? payload.session : {}; const messages = Array.isArray(session.messages) ? session.messages : []; const stored = Number(session.message_count); const count = Number.isFinite(stored) && stored > 0 ? stored : messages.length; const working = Boolean(session.is_streaming) || Boolean(session.active_stream_id) || Boolean(session.has_pending_user_message); if (working) show('Hermes is working. Latest stored activity: ' + activityLabel(messages) + '.', true, false); else if (!count && Date.now() - started >= 4000) show('This session has no renderable messages yet. It may be new or no longer available.', false, true); else hide(); schedule(document.hidden ? 15000 : 3000); } catch (_) { show('Session updates disconnected. Retrying without changing this session…', true, false); schedule(document.hidden ? 15000 : 3000); } finally { clearTimeout(timeout); request = null; } } addEventListener('online', () => schedule(0)); addEventListener('pageshow', () => schedule(0)); document.addEventListener('visibilitychange', () => schedule(0)); schedule(1200); })();`