- 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
81 lines
3.0 KiB
Python
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
|