jenkins d041f1d1ee hermes(voice): round-3 fixes, language routing, session-toast root cause
- Captions read message bodies only (the scraper was concatenating
  avatar, author and worklog chips); multi-segment interim turns are
  now speakable and drive clean speak-to-thinking-to-speak cycles when
  playback drains mid-turn.
- Dynamic endpointing: complete-looking partials (3+ words or terminal
  punctuation) endpoint at the base window; the long hold remains only
  for one-two-word fragments. A stale-busy 10s settle wait on every
  post-error send is gone.
- Both overlay captions are bounded, touch-scrollable regions with
  follow-tail; caps raised for long turns.
- Error envelopes are never spoken or captioned; errored turns run
  resyncCapture (fresh STT session on the hot mic).
- Workspace toggle now lives in the sidebar rail (floating button only
  below the rail breakpoint).
- False 'session unavailable' toast root-caused: the router continuity
  guard shows it on a 409 that fired when a transient profile-listing
  failure failed closed into a fake cross-profile mismatch; the patcher
  now answers from the alias cache and never claims a default-vs-named
  mismatch while aliases are unconfirmed.
- Barge-in sends carry a one-line cut-point marker with the last spoken
  sentence; visible-history truncation judged infeasible client-side.
- Language switching works end-to-end: sticky per-session STT language
  hint (restarting an unused next session on switch), reply voice from
  script evidence, STT detection, then stopword heuristic; cues and WAV
  fallback share the turn language.
- Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for
  the container-fallback test. 272 voice-lane tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00

471 lines
24 KiB
JavaScript

/* node:coverage disable */
(function (root, factory) {
'use strict';
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
else {
root.HermesHuxBootstrap = api;
api.autoStart(root);
}
}(typeof globalThis === 'object' ? globalThis : this, function () {
/* node:coverage enable */
'use strict';
const ACCEPT = 'application/vnd.hermes.hux+json; version=1';
const ID = /^[a-z]{2,6}_[A-Za-z0-9._-]{4,80}$/;
const SLOT = /^slot-[0-9]{1,3}$/;
const USER = /^usr_[0-9a-f]{16,64}$/;
const WEBUI_SESSION = /^[A-Za-z0-9._:-]{4,160}$/;
const PROJECT_SOURCE = /^[A-Za-z0-9._:@+-]{1,200}$/;
const STREAM = /^[A-Za-z0-9._:-]{1,120}$/;
const TRUST = new Set(['router', 'relay', 'worker']);
const SURFACE = new Set(['chat', 'worker', 'telegram', 'voice', 'api']);
const CARD_FLAGS = Object.freeze({
'HUX-11': 'hux.foundation', 'HUX-01': 'hux.activity_timeline',
'HUX-02': 'hux.memory_control', 'HUX-03': 'hux.projects',
'HUX-04': 'hux.artifacts', 'HUX-05': 'hux.autonomy',
'HUX-06': 'hux.friendly_modes', 'HUX-07': 'hux.multimodal',
'HUX-08': 'hux.research', 'HUX-09': 'hux.onboarding',
'HUX-10': 'hux.privacy', 'HUX-12': 'hux.release_followthrough',
});
const REQUIRED_ROUTES = Object.freeze({
'HUX-11': [['HUX-11', '/hux/v1/capabilities'],
['HUX-11', '/hux/v1/context/bootstrap']],
'HUX-01': [['HUX-01', '/hux/v1/conversations/{id}/events']],
'HUX-02': [['HUX-02', '/hux/v1/memory'], ['HUX-02', '/hux/v1/memory/{id}/{action}']],
'HUX-03': [['HUX-03', '/hux/v1/projects/{id}'], ['HUX-03', '/hux/v1/conversations'],
['HUX-03', '/hux/v1/conversations/{id}'], ['HUX-03', '/hux/v1/conversations/{id}/lineage']],
'HUX-04': [['HUX-04', '/hux/v1/artifacts'], ['HUX-04', '/hux/v1/artifacts/{id}/versions'],
['HUX-04', '/hux/v1/artifacts/{id}/promote']],
'HUX-05': [['HUX-05', '/hux/v1/policy'], ['HUX-05', '/hux/v1/approvals'],
['HUX-05', '/hux/v1/approvals/{id}'], ['HUX-05', '/hux/v1/runs/{id}/stop'],
['HUX-05', '/hux/v1/runs/{id}/budget'], ['HUX-05', '/hux/v1/runs/{id}/gate']],
'HUX-06': [['HUX-06', '/hux/v1/modes'],
['HUX-06', '/hux/v1/projects/{project_id}/conversations/{id}/mode']],
'HUX-07': [['HUX-07', '/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items'],
['HUX-07', '/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}'],
['HUX-07', '/hux/v1/projects/{project_id}/conversations/{id}/multimodal/items/{item_id}/transcript-corrections'],
['HUX-07', '/hux/v1/projects/{project_id}/conversations/{id}/capture-intents']],
'HUX-08': [['HUX-08', '/hux/v1/messages/{id}/citations'], ['HUX-08', '/hux/v1/notebooks'],
['HUX-08', '/hux/v1/notebooks/{id}']],
'HUX-09': [['HUX-09', '/hux/v1/projects/{project_id}/conversations/{id}/suggestions/evaluate'],
['HUX-09', '/hux/v1/projects/{project_id}/conversations/{id}/suggestions/{suggestion_id}/decisions'],
['HUX-09', '/hux/v1/projects/{project_id}/conversations/{id}/suggestions/states']],
'HUX-10': [['HUX-10', '/hux/v1/privacy/policy'], ['HUX-10', '/hux/v1/privacy/notices'],
['HUX-10', '/hux/v1/conversations/{id}/forget'], ['HUX-10', '/hux/v1/privacy/audit']],
'HUX-12': [['HUX-12', '/hux/v1/projects/{project_id}/conversations/{id}/releases'],
['HUX-12', '/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}'],
['HUX-12', '/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}/transitions']],
});
const WAVES = Object.freeze({
a: ['HUX-01', 'HUX-02'], b: ['HUX-03', 'HUX-04', 'HUX-06', 'HUX-08'],
governance: ['HUX-05', 'HUX-10'], c: ['HUX-07', 'HUX-09', 'HUX-12'],
});
function record(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
}
function immutable(value) {
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
Object.values(value).forEach(immutable);
return Object.freeze(value);
}
function opaque(value, prefix) {
return typeof value === 'string' && ID.test(value) && value.startsWith(`${prefix}_`);
}
function normalizeIdentity(value) {
if (!exact(value, ['tenant_slot', 'subject', 'surface', 'trust']) ||
!SLOT.test(value.tenant_slot) || !USER.test(value.subject) ||
!SURFACE.has(value.surface) || !TRUST.has(value.trust)) return null;
return immutable({tenantSlot: value.tenant_slot, userRef: value.subject,
surface: value.surface, trust: value.trust});
}
function normalizeContext(session) {
const value = record(session);
const context = value && record(value.hux_context);
const contextKeys = new Set(['schema', 'webui_session_id', 'session_id', 'conversation_id',
'project_id', 'project_source', 'identity', 'run_id', 'message_id',
'branch_point_message_id', 'notebook_id']);
if (!context || context.schema !== 'hux.webui_context.v1' ||
Object.keys(context).some((key) => !contextKeys.has(key)) ||
!WEBUI_SESSION.test(String(value.session_id || '')) ||
context.webui_session_id !== value.session_id || !opaque(context.session_id, 'ses') ||
!opaque(context.conversation_id, 'conv') || !opaque(context.project_id, 'prj') ||
!PROJECT_SOURCE.test(String(context.project_source || ''))) return null;
const identity = normalizeIdentity(context.identity);
if (!identity) return null;
const optional = {runId: context.run_id, messageId: context.message_id,
branchPointMessageId: context.branch_point_message_id, notebookId: context.notebook_id};
if (optional.runId !== undefined && !opaque(optional.runId, 'run')) return null;
for (const name of ['messageId', 'branchPointMessageId']) {
if (optional[name] !== undefined && !opaque(optional[name], 'msg')) return null;
}
if (optional.notebookId !== undefined && !opaque(optional.notebookId, 'nb')) return null;
return immutable({webuiSessionId: value.session_id, sessionId: context.session_id,
conversationId: context.conversation_id, projectId: context.project_id,
projectSource: context.project_source, identity, ...optional});
}
function sameIdentity(raw, expected) {
return Boolean(exact(raw, ['tenant_slot', 'subject', 'surface', 'trust']) &&
raw.tenant_slot === expected.tenantSlot &&
raw.subject === expected.userRef && raw.surface === expected.surface && raw.trust === expected.trust);
}
function exact(value, keys) {
return Boolean(record(value) && Object.keys(value).length === keys.length &&
keys.every((key) => Object.prototype.hasOwnProperty.call(value, key)));
}
function normalizeBootstrap(payload, context, status) {
const keys = ['schema', 'contract_version', 'identity', 'session_id', 'project_id',
'conversation_id', 'revisions', 'created'];
if (!exact(payload, keys) || payload.schema !== 'hux.context_bootstrap.v1' ||
payload.contract_version !== '1.1.0' || !sameIdentity(payload.identity, context.identity) ||
payload.session_id !== context.sessionId || payload.project_id !== context.projectId ||
payload.conversation_id !== context.conversationId ||
!exact(payload.revisions, ['project', 'conversation']) ||
!exact(payload.created, ['project', 'conversation'])) return null;
const revisions = payload.revisions; const created = payload.created;
if (!Number.isSafeInteger(revisions.project) || revisions.project < 1 ||
!Number.isSafeInteger(revisions.conversation) || revisions.conversation < 1 ||
typeof created.project !== 'boolean' || typeof created.conversation !== 'boolean' ||
(status === 201) !== (created.project || created.conversation)) return null;
return immutable({revisions: {...revisions}, created: {...created}});
}
function normalizeCapabilities(payload, expected) {
if (!exact(payload, ['schema', 'contract_version', 'identity', 'cards', 'server']) ||
payload.schema !== 'hux.capabilities.v1' ||
!/^1\.\d+\.\d+$/.test(String(payload.contract_version)) ||
!sameIdentity(payload.identity, expected) || !Array.isArray(payload.cards) ||
payload.cards.length > Object.keys(CARD_FLAGS).length || !record(payload.server) ||
Object.keys(payload.server).some((key) => !['commit', 'image_digest'].includes(key)) ||
Object.values(payload.server).some((item) => typeof item !== 'string' || item.length > 160)) return null;
const cards = {};
for (const item of payload.cards) {
if (!exact(item, ['card', 'flag', 'enabled', 'routes']) ||
!Object.prototype.hasOwnProperty.call(CARD_FLAGS, item.card) ||
CARD_FLAGS[item.card] !== item.flag || typeof item.enabled !== 'boolean' ||
!Array.isArray(item.routes) || cards[item.card]) return null;
const routes = item.routes.filter((route) => typeof route === 'string' &&
/^\/hux\/v1(?:\/[A-Za-z0-9._:{}-]+)*$/.test(route));
if (routes.length !== item.routes.length || new Set(routes).size !== routes.length) return null;
cards[item.card] = immutable({enabled: item.enabled, routes});
}
if (!cardComplete(cards, 'HUX-11')) return null;
return immutable(cards);
}
function cardComplete(cards, card) {
const current = cards[card];
if (!current || !current.enabled) return false;
return REQUIRED_ROUTES[card].every(([owner, route]) =>
cards[owner] && cards[owner].enabled && cards[owner].routes.includes(route));
}
function enabledWaves(cards) {
const result = [];
Object.entries(WAVES).forEach(([wave, members]) => {
const enabled = members.filter((card) => cards[card] && cards[card].enabled);
if (enabled.length && enabled.every((card) => cardComplete(cards, card))) result.push(wave);
});
return Object.freeze(result);
}
function safeCsrf(config) {
const value = record(config) && config.csrfToken;
return typeof value === 'string' && value.length >= 16 && value.length <= 512 &&
!/[\u0000-\u001f\u007f]/.test(value) ? value : null;
}
function scopedFetcher(fetcher, csrfToken) {
return function huxFetch(url, init) {
const target = String(url || '');
if (!/^\/hux\/v1(?:[/?]|$)/.test(target) || target.startsWith('//') || target.includes('..')) {
return Promise.reject(new TypeError('HUX browser requests must remain same-origin'));
}
const options = init || {};
const method = String(options.method || 'GET').toUpperCase();
const headers = {...(options.headers || {})};
if (!['GET', 'HEAD'].includes(method)) headers['X-Hermes-CSRF-Token'] = csrfToken;
return fetcher(target, {...options, cache: 'no-store', credentials: 'same-origin', headers});
};
}
async function preflight(fetcher, context) {
try {
const response = await fetcher('/hux/v1/capabilities', {method: 'GET',
headers: {Accept: ACCEPT}});
if (!response || response.status === 404 || !response.ok) return null;
const cards = normalizeCapabilities(await response.json(), context.identity);
const waves = cards && enabledWaves(cards);
return waves && waves.length ? immutable({cards, waves}) : null;
} catch (_) {
return null;
}
}
async function bootstrapContext(fetcher, context) {
try {
const response = await fetcher('/hux/v1/context/bootstrap', {method: 'POST', headers: {
Accept: ACCEPT, 'Content-Type': 'application/json',
'Idempotency-Key': `hux:context:${context.sessionId}`,
}, body: JSON.stringify({raw_session_id: context.webuiSessionId,
project_source: context.projectSource, session_id: context.sessionId,
conversation_id: context.conversationId, project_id: context.projectId})});
if (!response || ![200, 201].includes(response.status) || !response.ok) return null;
return normalizeBootstrap(await response.json(), context, response.status);
} catch (_) {
return null;
}
}
function node(doc, tag, attributes, text) {
const value = doc.createElement(tag);
Object.entries(attributes).forEach(([name, item]) => value.setAttribute(name, item));
if (text !== undefined) value.textContent = String(text);
return value;
}
function railHost(doc) {
// The app's left icon rail (chat, tasks, telegram, …). Optional: bundles
// without it (and headless harnesses) keep the floating toggle.
if (typeof doc.querySelector !== 'function') return null;
const rail = doc.querySelector('nav.rail');
return rail && typeof rail.insertBefore === 'function' ? rail : null;
}
function railIcon(doc) {
try {
if (typeof doc.createElementNS !== 'function') return null;
const svgNS = 'http://www.w3.org/2000/svg';
const svg = doc.createElementNS(svgNS, 'svg');
[['width', '20'], ['height', '20'], ['viewBox', '0 0 24 24'], ['fill', 'none'],
['stroke', 'currentColor'], ['stroke-width', '1.5'], ['stroke-linecap', 'round'],
['stroke-linejoin', 'round'], ['aria-hidden', 'true'],
].forEach(([name, value]) => svg.setAttribute(name, value));
const path = doc.createElementNS(svgNS, 'path');
path.setAttribute('d', 'M3 5a2 2 0 0 1 2-2h6v18H5a2 2 0 0 1-2-2V5Zm10-2h6a2 2 0 0 1 2 2v5h-8V3Zm0 11h8v5a2 2 0 0 1-2 2h-6v-7Z');
svg.appendChild(path);
return svg;
} catch (_) {
return null;
}
}
function createChrome(doc) {
const rail = railHost(doc);
const open = node(doc, 'button', {'class': rail ?
'hux-workspace-toggle hux-workspace-toggle--railed' : 'hux-workspace-toggle',
type: 'button', 'aria-controls': 'huxWorkspaceDrawer', 'aria-expanded': 'false'}, 'Workspace');
let railToggle = null;
if (rail) {
// Rail placement: an icon-only item beside the app's own tabs, styled
// by the upstream .rail-btn/.has-tooltip rules. The floating button
// stays in the DOM as the below-rail-breakpoint fallback and is hidden
// by bootstrap.css wherever the rail is visible, so nothing floats
// over the chat. Drawer behavior is unchanged.
railToggle = node(doc, 'button', {'class': 'rail-btn has-tooltip hux-workspace-toggle-rail',
type: 'button', 'aria-controls': 'huxWorkspaceDrawer', 'aria-expanded': 'false',
'data-tooltip': 'Workspace', 'aria-label': 'Workspace'});
const icon = railIcon(doc);
if (icon) railToggle.appendChild(icon);
else railToggle.textContent = '⧉';
const spacer = typeof rail.querySelector === 'function' ? rail.querySelector('.rail-spacer') : null;
rail.insertBefore(railToggle, spacer || null);
}
const drawer = node(doc, 'aside', {id: 'huxWorkspaceDrawer', 'class': 'hux-workspace-drawer',
'aria-labelledby': 'huxWorkspaceTitle'});
const header = node(doc, 'header', {'class': 'hux-workspace-drawer__header'});
const title = node(doc, 'h2', {id: 'huxWorkspaceTitle'}, 'Hermes workspace');
const close = node(doc, 'button', {type: 'button', 'aria-label': 'Close Hermes workspace'}, 'Close');
const content = node(doc, 'div', {'class': 'hux-workspace-drawer__content'});
drawer.hidden = true;
header.appendChild(title); header.appendChild(close); drawer.appendChild(header); drawer.appendChild(content);
doc.body.appendChild(open); doc.body.appendChild(drawer);
function setOpen(value) {
drawer.hidden = !value; open.setAttribute('aria-expanded', value ? 'true' : 'false');
if (railToggle) railToggle.setAttribute('aria-expanded', value ? 'true' : 'false');
if (value && typeof close.focus === 'function') close.focus();
}
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
const onKey = (event) => { if (event.key === 'Escape' && !drawer.hidden) setOpen(false); };
open.addEventListener('click', onOpen); close.addEventListener('click', onClose);
if (railToggle) railToggle.addEventListener('click', onOpen);
doc.addEventListener('keydown', onKey);
return Object.freeze({content, drawer, open, railToggle, setOpen, destroy() {
doc.removeEventListener('keydown', onKey);
if (open.parentNode) open.parentNode.removeChild(open);
if (railToggle && railToggle.parentNode) railToggle.parentNode.removeChild(railToggle);
if (drawer.parentNode) drawer.parentNode.removeChild(drawer);
}});
}
async function defaultRuntimeFactory(root, target, context, fetcher, waves, controls) {
const runtimes = [];
const common = {document: root.document, fetcher, expectedIdentity: context.identity,
projectId: context.projectId, conversationId: context.conversationId,
sessionId: context.sessionId, runId: context.runId, messageId: context.messageId,
branchPointMessageId: context.branchPointMessageId, notebookId: context.notebookId,
stopModelResponse: controls && controls.stopModelResponse,
canStopModelResponse: controls && controls.canStopModelResponse};
const constructors = {
a: root.HermesHuxWaveA && root.HermesHuxWaveA.createWaveARuntime,
b: root.HermesHuxWaveBRuntime && root.HermesHuxWaveBRuntime.createWaveBRuntime,
governance: root.HermesHuxAutonomyPrivacy &&
root.HermesHuxAutonomyPrivacy.createAutonomyPrivacyRuntime,
c: root.HermesHuxWaveC && root.HermesHuxWaveC.createWaveCRuntime,
};
try {
for (const wave of waves) {
if (typeof constructors[wave] !== 'function') throw new Error('HUX runtime is incomplete');
const host = node(root.document, 'section', {'class': `hux-workspace-wave hux-workspace-wave--${wave}`});
target.appendChild(host);
const runtime = constructors[wave](common);
runtimes.push(runtime);
await runtime.mount(host);
}
} catch (error) {
runtimes.reverse().forEach((runtime) => runtime.destroy());
throw error;
}
return Object.freeze({destroy() { runtimes.reverse().forEach((runtime) => runtime.destroy()); }});
}
function ownedActiveStream(session, activeStreamId, expected) {
const current = normalizeContext(session);
const raw = record(session);
return current && expected && current.webuiSessionId === expected.webuiSessionId &&
current.conversationId === expected.conversationId &&
current.identity.userRef === expected.identity.userRef && STREAM.test(String(activeStreamId || '')) &&
raw.active_stream_id === activeStreamId ? activeStreamId : null;
}
function createOwnedStop(root, sessionReader, activeStreamReader, context, timeoutMs) {
const bounded = Number.isSafeInteger(timeoutMs) && timeoutMs >= 250 && timeoutMs <= 10000 ? timeoutMs : 3000;
const canStopModelResponse = () => Boolean(ownedActiveStream(sessionReader(), activeStreamReader(), context) &&
typeof root.cancelStream === 'function');
async function stopModelResponse() {
const streamId = ownedActiveStream(sessionReader(), activeStreamReader(), context);
if (!streamId || typeof root.cancelStream !== 'function') return Object.freeze({accepted: false});
let timer = null;
try {
const timeout = new Promise((resolve) => { timer = root.setTimeout(() => resolve(false), bounded); });
const accepted = await Promise.race([Promise.resolve(root.cancelStream('hux-stop')), timeout]);
if (timer !== null) root.clearTimeout(timer);
return Object.freeze({accepted: accepted === true, sessionId: context.webuiSessionId, streamId});
} catch (_) {
if (timer !== null) root.clearTimeout(timer);
return Object.freeze({accepted: false});
}
}
return Object.freeze({canStopModelResponse, stopModelResponse});
}
function createCoordinator(options) {
const settings = options || {};
const root = settings.root;
const doc = settings.document || (root && root.document);
const rawFetch = settings.fetcher || (root && root.fetch && root.fetch.bind(root));
const sessionReader = settings.sessionReader;
const activeStreamReader = settings.activeStreamReader || (() => browserActiveStreamReader());
const configReader = settings.configReader || (() => root.__HERMES_CONFIG__);
const runtimeFactory = settings.runtimeFactory || ((target, context, fetcher, waves, controls) =>
defaultRuntimeFactory(root, target, context, fetcher, waves, controls));
if (!root || !doc || !rawFetch || typeof sessionReader !== 'function') {
throw new TypeError('HUX coordinator requires browser, fetch, and trusted session reader');
}
let fingerprint = null; let pendingFingerprint = null; let generation = 0;
let chrome = null; let runtime = null; let timer = null;
function clear() {
if (runtime) runtime.destroy();
if (chrome) chrome.destroy();
runtime = null; chrome = null;
}
async function refresh() {
const context = normalizeContext(sessionReader());
const activeStream = context ? ownedActiveStream(sessionReader(), activeStreamReader(), context) : null;
const next = context ? `${context.webuiSessionId}\0${context.conversationId}\0${context.identity.userRef}` +
`\0${context.runId || ''}\0${activeStream || ''}` : '';
if (next === fingerprint || next === pendingFingerprint) return false;
pendingFingerprint = next; const current = ++generation; clear();
if (!context) { fingerprint = ''; pendingFingerprint = null; return false; }
const csrf = safeCsrf(configReader());
if (!csrf) { pendingFingerprint = null; return false; }
const fetcher = scopedFetcher(rawFetch, csrf);
const capability = await preflight(fetcher, context);
if (current !== generation) return false;
if (!capability) { pendingFingerprint = null; return false; }
const binding = await bootstrapContext(fetcher, context);
if (current !== generation) return false;
if (!binding) { pendingFingerprint = null; return false; }
const nextChrome = createChrome(doc);
try {
const controls = createOwnedStop(root, sessionReader, activeStreamReader, context, settings.stopTimeoutMs);
const nextRuntime = await runtimeFactory(nextChrome.content, context, fetcher, capability.waves, controls);
if (current !== generation) { nextRuntime.destroy(); nextChrome.destroy(); return false; }
chrome = nextChrome; runtime = nextRuntime; fingerprint = next;
pendingFingerprint = null; return true;
} catch (_) {
nextChrome.destroy(); pendingFingerprint = null; return false;
}
}
function start() {
if (timer !== null) return;
void refresh();
timer = root.setInterval(() => { void refresh(); }, settings.pollMs || 750);
root.addEventListener('pageshow', refresh);
}
function destroy() {
generation += 1; fingerprint = null; pendingFingerprint = null;
if (timer !== null) root.clearInterval(timer);
timer = null; root.removeEventListener('pageshow', refresh); clear();
}
return Object.freeze({destroy, refresh, start, getState: () => ({active: Boolean(runtime), fingerprint})});
}
function browserSessionReader() {
return typeof S === 'object' ? S.session : null;
}
function browserActiveStreamReader() {
return typeof S === 'object' ? S.activeStreamId : null;
}
function mergeTrustedContext(session, payload) {
const current = record(session); const response = record(payload);
if (!current || !response || response.session_id !== current.session_id ||
!record(response.hux_context)) return false;
const next = normalizeContext({...current, hux_context: response.hux_context});
if (!next || next.webuiSessionId !== current.session_id) return false;
current.hux_context = response.hux_context;
return true;
}
function autoStart(root) {
if (!root || !root.document || typeof root.fetch !== 'function') return null;
const coordinator = createCoordinator({root, sessionReader: browserSessionReader});
if (root.document.readyState === 'loading') {
root.document.addEventListener('DOMContentLoaded', () => coordinator.start(), {once: true});
} else coordinator.start();
return coordinator;
}
return Object.freeze({ACCEPT, autoStart, browserActiveStreamReader, cardComplete, createChrome,
createCoordinator, createOwnedStop, ownedActiveStream,
bootstrapContext, defaultRuntimeFactory, enabledWaves, normalizeBootstrap,
normalizeCapabilities, normalizeContext, normalizeIdentity, mergeTrustedContext,
preflight, safeCsrf, scopedFetcher});
}));