// Deterministic assistant-response EXTRACTION probe for // dockerfiles/hermes-webui-atlas-voice.js. // // Round 3's caption/TTS extraction was validated only against a single synthetic // {dataset:{rawText}} node, so it never exercised the REAL rendered assistant // turn and shipped a turn.textContent fallback that scraped the avatar "H", the // "Hermes" author name and the "Processed 13s" worklog chip into the // conversation caption and the one-ahead TTS synthesizer ("HHermesProcessed // 13s"), and truncated multi-sentence replies to their first sentence. // // This probe builds a FAITHFUL rendered turn — matching the live build-24 DOM // (ui.js _createAssistantTurn / renderMessages, messages.js ensureAssistantRow): // //
//
//
H
// Hermes //
//
//
…Processed 13s…
// //
//
REASONING
//
ANSWER
//
//
//
// // with a real querySelectorAll / closest / matches implementation, then drives // the actual exported extraction + one-ahead chunker and asserts: // (i) caption/TTS text is the answer BODY only — never "H"/"Hermes"/"Processed" // (ii) every sentence of a multi-sentence reply reaches the chunk queue // (iii) a pre-settle worklog-only frame extracts to '' (so the final pump // retries rather than finalizing garbage into "Listening") // // node hermes_voice_response_probe.js 'use strict'; const fs = require('fs'); const vm = require('vm'); const SCRIPT_PATH = process.argv[2]; if (!SCRIPT_PATH) { throw new Error('usage: hermes_voice_response_probe.js '); } const SOURCE = fs.readFileSync(SCRIPT_PATH, 'utf8'); // ── Faithful minimal DOM ──────────────────────────────────────────────────── // Supports exactly what the extraction touches: className/classList, dataset // (backed by data-* attributes so `[data-raw-text]` selects), hidden, recursive // textContent, appendChild/children/parentNode, setAttribute/getAttribute, and // querySelector(All)/closest/matches over comma-separated compound selectors of // tag / .class / [attr] / [attr="value"] terms (no combinators — none are used). function parseSelector(selector) { return String(selector).split(',').map((group) => { const term = group.trim(); const parts = []; const re = /([.#]?[\w-]+)|\[([\w-]+)(?:([~|^$*]?=)"?([^"\]]*)"?)?\]/g; let m; while ((m = re.exec(term))) { if (m[1]) { if (m[1][0] === '.') parts.push({ kind: 'class', value: m[1].slice(1) }); else if (m[1][0] === '#') parts.push({ kind: 'id', value: m[1].slice(1) }); else parts.push({ kind: 'tag', value: m[1].toLowerCase() }); } else if (m[2]) { parts.push({ kind: 'attr', name: m[2], op: m[3] || null, value: m[4] }); } } return parts; }).filter((parts) => parts.length); } class Node { constructor(tag) { this.tag = String(tag || 'div').toLowerCase(); this.className = ''; this.attributes = new Map(); this.children = []; this.parentNode = null; this.hidden = false; this._text = ''; const self = this; this.dataset = new Proxy({}, { get(_t, key) { if (typeof key !== 'string') return undefined; const attr = 'data-' + key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); return self.attributes.has(attr) ? self.attributes.get(attr) : undefined; }, set(_t, key, value) { const attr = 'data-' + String(key).replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); self.attributes.set(attr, String(value)); return true; }, has(_t, key) { const attr = 'data-' + String(key).replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); return self.attributes.has(attr); }, }); this.style = { setProperty() {}, removeProperty() {}, getPropertyValue() { return ''; }, display: '' }; this.listeners = new Map(); } get classList() { const el = this; return { add(name) { const s = new Set(el.className.split(/\s+/).filter(Boolean)); s.add(name); el.className = [...s].join(' '); }, remove(name) { el.className = el.className.split(/\s+/).filter((v) => v && v !== name).join(' '); }, contains(name) { return el.className.split(/\s+/).indexOf(name) >= 0; }, toggle(name, force) { if (force === undefined ? this.contains(name) : !force) this.remove(name); else this.add(name); }, }; } setAttribute(name, value) { this.attributes.set(name, String(value)); if (name === 'class') this.className = String(value); if (name === 'hidden') this.hidden = true; } getAttribute(name) { if (name === 'class') return this.className || null; return this.attributes.has(name) ? this.attributes.get(name) : null; } appendChild(child) { child.parentNode = this; this.children.push(child); return child; } addEventListener(type, cb) { this.listeners.set(type, cb); } removeEventListener(type) { this.listeners.delete(type); } set textContent(value) { this._text = String(value); this.children = []; } get textContent() { if (this.children.length) return this.children.map((c) => c.textContent).join(''); return this._text; } _matchesTerm(parts) { return parts.every((p) => { if (p.kind === 'class') return this.classList.contains(p.value); if (p.kind === 'tag') return this.tag === p.value; if (p.kind === 'id') return this.getAttribute('id') === p.value; if (p.kind === 'attr') { if (!this.attributes.has(p.name)) return false; if (!p.op) return true; return this.attributes.get(p.name) === p.value; } return false; }); } matches(selector) { return parseSelector(selector).some((parts) => this._matchesTerm(parts)); } _walk(out) { for (const c of this.children) { out.push(c); c._walk(out); } return out; } querySelectorAll(selector) { const groups = parseSelector(selector); return this._walk([]).filter((node) => groups.some((parts) => node._matchesTerm(parts))); } querySelector(selector) { const all = this.querySelectorAll(selector); return all.length ? all[0] : null; } closest(selector) { const groups = parseSelector(selector); let node = this; while (node) { if (groups.some((parts) => node._matchesTerm(parts))) return node; node = node.parentNode; } return null; } } function el(tag, className, attrs, text) { const node = new Node(tag); if (className) node.setAttribute('class', className); if (attrs) Object.keys(attrs).forEach((k) => node.setAttribute(k, attrs[k])); if (text !== undefined) node.textContent = text; return node; } // A role header (avatar "H" + "Hermes") — the source of the "HHermes" leak. function roleHeader() { const role = el('div', 'msg-role assistant'); role.appendChild(el('div', 'role-icon assistant', null, 'H')); role.appendChild(el('span', 'msg-role-name', null, 'Hermes')); return role; } // The worklog "Processed 13s" chip — the source of the "Processed 13s" leak. function worklogGroup() { const group = el('div', 'tool-worklog-group tool-call-group', { 'data-anchor-scene-owner': '1' }); const summary = el('button', 'tool-call-group-summary tool-worklog-summary'); summary.appendChild(el('span', 'tool-call-group-label', null, 'Processed')); summary.appendChild(el('span', 'tool-call-group-duration', null, ' 13s')); group.appendChild(summary); const body = el('div', 'tool-call-group-body tool-worklog-body', { hidden: 'hidden' }); body.appendChild(el('div', 'wl-reason', null, 'internal tool reasoning that must never be spoken')); group.appendChild(body); return group; } function answerSegment(rawText, bodyText, opts) { const seg = el('div', 'assistant-segment', { 'data-msg-idx': String((opts && opts.idx) || 1) }); if (rawText !== null && rawText !== undefined) seg.setAttribute('data-raw-text', rawText); if (opts && opts.live) seg.setAttribute('data-live-assistant', '1'); // Reasoning rendered INSIDE the segment uses .thinking-card-body, but pin the // chrome exclusion by nesting a stray .msg-body inside a thinking card too. if (opts && opts.reasoning) { const card = el('div', 'thinking-card'); card.appendChild(el('div', 'msg-body', null, 'REASONING: ' + opts.reasoning)); seg.appendChild(card); } if (bodyText !== null && bodyText !== undefined) seg.appendChild(el('div', 'msg-body', null, bodyText)); return seg; } function assistantTurn(segments, opts) { const turn = el('div', 'msg-row assistant-turn', { 'data-role': 'assistant', 'data-session-id': 'session-1' }); if (opts && opts.live) turn.setAttribute('id', 'liveAssistantTurn'); turn.appendChild(roleHeader()); const blocks = el('div', 'assistant-turn-blocks'); if (opts && opts.worklog) blocks.appendChild(worklogGroup()); segments.forEach((s) => blocks.appendChild(s)); turn.appendChild(blocks); return turn; } // ── vm harness ────────────────────────────────────────────────────────────── const documentRoot = el('div', 'msgInner'); function makeControl(id) { const node = new Node('div'); node.setAttribute('id', id); return node; } const controls = { btnVoiceMode: makeControl('btnVoiceMode'), voiceModeBar: makeControl('voiceModeBar'), voiceModeIndicator: makeControl('voiceModeIndicator'), voiceModeLabel: makeControl('voiceModeLabel'), msg: makeControl('msg'), }; const documentStub = { baseURI: 'https://chat.test/', body: new Node('body'), getElementById: (id) => controls[id] || (id === 'liveAssistantTurn' ? documentRoot.querySelector('#liveAssistantTurn') : null), querySelectorAll: (selector) => documentRoot.querySelectorAll(selector), querySelector: (selector) => documentRoot.querySelector(selector), createElement: (tag) => new Node(tag), addEventListener() {}, removeEventListener() {}, }; const sandbox = { console, window: null, document: documentStub, navigator: { mediaDevices: { getSupportedConstraints: () => ({}), getUserMedia: async () => ({ getTracks: () => [], getAudioTracks: () => [] }) } }, MediaRecorder: function MediaRecorder() {}, matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }), localStorage: { getItem: () => null, setItem() {}, removeItem() {} }, crypto: { getRandomValues(b) { for (let i = 0; i < b.length; i += 1) b[i] = i + 1; return b; } }, location: { protocol: 'https:', host: 'chat.test', href: 'https://chat.test/' }, fetch: () => Promise.reject(new Error('no network in probe')), setTimeout: () => 0, clearTimeout: () => {}, setInterval: () => 0, clearInterval: () => {}, queueMicrotask: (fn) => Promise.resolve().then(fn), S: { session: { session_id: 'session-1' }, busy: false, activeStreamId: null }, URL, Math, JSON, String, Number, Boolean, Error, Array, Object, Set, Map, parseInt, parseFloat, isNaN, Date, }; sandbox.window = sandbox; sandbox.globalThis = sandbox; sandbox.window.matchMedia = sandbox.matchMedia; sandbox.window.MediaRecorder = sandbox.MediaRecorder; vm.createContext(sandbox); vm.runInContext(SOURCE, sandbox, { filename: 'atlas-voice.js' }); const internals = sandbox.window.__atlasVoiceInternals; if (!internals) { throw new Error('atlas-voice.js did not expose __atlasVoiceInternals (early return? missing DOM stub)'); } function setTurn(turn) { documentRoot.children = []; if (turn) documentRoot.appendChild(turn); } // Mirror pumpAssistantResponse's one-ahead consume loop over a full reply: a // partial pass (isFinal=false) then the completion pass (isFinal=true) + tail // flush. Proves every sentence is chunked, not just sentence one. function pumpChunks(text) { let consumed = 0; let first = true; const chunks = []; for (let pass = 0; pass < 12; pass += 1) { const remaining = text.slice(consumed).replace(/^\s+/, ''); const skipped = text.slice(consumed).length - remaining.length; const ex = internals.adaptiveChunks(remaining, false, first); if (!ex.chunks.length) break; first = false; consumed += skipped + ex.consumed; chunks.push(...ex.chunks); } // completion pass { const remaining = text.slice(consumed).replace(/^\s+/, ''); const skipped = text.slice(consumed).length - remaining.length; const ex = internals.adaptiveChunks(remaining, true, first); if (ex.chunks.length) { consumed += skipped + ex.consumed; chunks.push(...ex.chunks); } const tail = text.slice(consumed).trim(); if (tail) { chunks.push(tail); consumed = text.length; } } return { chunks, consumed, covered: chunks.join(' ') }; } const results = {}; // (1) Finalized multi-segment turn: caption/TTS is answer-only, all sentences chunked. { const answer = 'Sentence one is here. Sentence two follows it. And sentence three concludes.'; const turn = assistantTurn([ answerSegment('earlier interim answer', 'earlier interim answer', { idx: 0 }), answerSegment(answer, answer, { idx: 1, reasoning: 'let me think' }), ], { worklog: true }); // fold the interim segment into the worklog exactly as the settle renderer does const interim = turn.querySelectorAll('.assistant-segment')[0]; interim.setAttribute('class', 'assistant-segment assistant-segment-worklog-source'); interim.setAttribute('aria-hidden', 'true'); interim.hidden = true; setTurn(turn); const extracted = internals.collectAssistantResponse(); const pumped = pumpChunks(extracted.text); results.finalized_multi_segment = { text: extracted.text, error: extracted.error, leaksAvatar: /HHermes|Hermes/.test(extracted.text), leaksProcessed: /Processed|13s/.test(extracted.text), leaksReasoning: /REASONING/.test(extracted.text), leaksInterim: /interim/.test(extracted.text), chunkCount: pumped.chunks.length, chunkConsumedAll: pumped.consumed === extracted.text.length, sentenceOnePresent: /one/.test(pumped.covered), sentenceTwoPresent: /two/.test(pumped.covered), sentenceThreePresent: /three/.test(pumped.covered), }; } // (2) Pre-settle worklog-only frame (the STREAM_DONE-beats-settle race): the // answer segment is not rendered yet — role header + "Processed 13s" only. // Must extract to '' so the final pump RETRIES instead of speaking chrome. { const turn = assistantTurn([], { worklog: true }); setTurn(turn); const extracted = internals.collectAssistantResponse(); results.presettle_worklog_only = { text: extracted.text, isEmpty: extracted.text === '', error: extracted.error, }; } // (3) Live streaming segment (no data-raw-text yet) — read via .msg-body. { const partial = 'The reply is still streaming right now'; const turn = assistantTurn([answerSegment(null, partial, { idx: 1, live: true })], { worklog: false, live: true }); setTurn(turn); const extracted = internals.collectAssistantResponse(); results.live_streaming_reads_body = { text: extracted.text, matches: extracted.text === partial, leaksAvatar: /Hermes/.test(extracted.text), }; } // (4) Reasoning + worklog chrome nested with stray .msg-body must be excluded. { const answer = 'Only this answer body should be spoken aloud.'; const seg = answerSegment(null, answer, { idx: 1, reasoning: 'hidden chain of thought' }); const turn = assistantTurn([seg], { worklog: true }); setTurn(turn); const extracted = internals.collectAssistantResponse(); results.chrome_excluded = { text: extracted.text, matches: extracted.text === answer, leaksReasoning: /REASONING|chain of thought/.test(extracted.text), }; } // (5) Streaming growth then finalize: extraction grows monotonically to the full // reply — the queue keeps receiving sentences until the turn is final. { const two = 'First sentence. Second sentence.'; const four = 'First sentence. Second sentence. Third sentence. Fourth sentence.'; const liveTurn = assistantTurn([answerSegment(null, two, { idx: 1, live: true })], { live: true }); setTurn(liveTurn); const partial = internals.collectAssistantResponse().text; const settledTurn = assistantTurn([answerSegment(four, four, { idx: 1 })], { worklog: true }); setTurn(settledTurn); const full = internals.collectAssistantResponse().text; const pumped = pumpChunks(full); results.streaming_growth = { partial, full, grows: full.length > partial.length && full.startsWith(partial), allFourChunked: /First/.test(pumped.covered) && /Second/.test(pumped.covered) && /Third/.test(pumped.covered) && /Fourth/.test(pumped.covered), consumedAll: pumped.consumed === full.length, }; } // (6) A turn with a recovered/transient error segment AND a real answer must // speak the answer (error must NOT veto a turn that produced content). { const answer = 'Here is the real answer despite a transient tool hiccup.'; const errSeg = answerSegment(null, 'a tool call failed transiently', { idx: 1 }); errSeg.setAttribute('data-error', '1'); const turn = assistantTurn([errSeg, answerSegment(answer, answer, { idx: 2 })], { worklog: false }); setTurn(turn); const extracted = internals.collectAssistantResponse(); results.error_segment_with_answer = { text: extracted.text, error: extracted.error, speaksAnswer: extracted.text === answer, }; } // (7) A turn that is ONLY an error envelope (no answer) still reports error. { const errSeg = answerSegment(null, 'The provider returned an error.', { idx: 1 }); errSeg.setAttribute('data-error', '1'); const turn = assistantTurn([errSeg], { worklog: false }); setTurn(turn); const extracted = internals.collectAssistantResponse(); results.error_only_reports_error = { text: extracted.text, isEmpty: extracted.text === '', error: extracted.error, }; } process.stdout.write(JSON.stringify(results, null, 2) + '\n');