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

81 lines
3.0 KiB
Python

"""Sliced upstream fixture: the exact api/profiles.py fragments the Atlas
voice patcher grafts into (session-continuity 409 resilience). Mirrors the
pinned Hermes WebUI deployment byte-for-byte for the anchored regions; see
dockerfiles/hermes-webui-atlas-patch.py."""
import logging
import threading
logger = logging.getLogger(__name__)
_root_profile_name_cache = {'default'}
_root_profile_name_cache_lock = threading.Lock()
_root_profile_name_cache_loaded = False
def list_profiles_api():
"""Fixture stand-in for the hermes_cli-backed profile listing."""
return []
def _is_root_profile(name: str) -> bool:
"""True if *name* resolves to the Hermes Agent root profile (~/.hermes).
Matches the legacy 'default' alias plus any name where list_profiles_api()
reports is_default=True. Memoized; call _invalidate_root_profile_cache()
after mutating profile metadata.
"""
global _root_profile_name_cache_loaded
if not name:
return False
if name == 'default':
return True
with _root_profile_name_cache_lock:
if _root_profile_name_cache_loaded:
return name in _root_profile_name_cache
# Cache miss — populate from list_profiles_api(). Done outside the lock to
# avoid holding it across a hermes_cli subprocess call.
try:
infos = list_profiles_api()
except Exception:
logger.debug("Failed to list profiles for root-profile lookup", exc_info=True)
return False
with _root_profile_name_cache_lock:
_root_profile_name_cache.clear()
_root_profile_name_cache.add('default')
for p in infos:
try:
if p.get('is_default') and p.get('name'):
_root_profile_name_cache.add(p['name'])
except (AttributeError, TypeError):
continue
_root_profile_name_cache_loaded = True
return name in _root_profile_name_cache
def _profiles_match(row_profile, active_profile) -> bool:
"""Return True if a session/project row's profile matches the active profile.
Treats both the literal alias 'default' and any renamed-root display name
(per _is_root_profile) as equivalent, so legacy rows tagged 'default'
still surface when the user has renamed the root profile to e.g. 'kinni',
and vice versa.
A row with no profile (`None` or empty string) is treated as belonging to
the root profile — that's the convention used by the legacy backfill at
api/models.py::all_sessions, and matches the default seen in
`static/sessions.js` (`S.activeProfile||'default'`).
Originally lived in api/routes.py; relocated here so both routes.py and
out-of-process consumers (mcp_server.py) can import the canonical helper
instead of duplicating the body. See #1614 for the visibility model.
"""
row = row_profile or 'default'
active = active_profile or 'default'
if row == active:
return True
# Cross-alias the renamed root.
if _is_root_profile(row) and _is_root_profile(active):
return True
return False