Preserve macOS app permissions via dynamic launcher, .env workspace path, and session/audio tools

This commit is contained in:
Adolfo Reyna
2026-08-11 21:15:37 -04:00
parent 583131221c
commit b5034b4b16
17 changed files with 1888 additions and 68 deletions
+48 -6
View File
@@ -95,16 +95,24 @@ def find_hermes_cli() -> str | None:
return shutil.which("hermes")
async def ensure_hermes_server(port: int = 8642) -> tuple[bool, str]:
"""Ensure Hermes gateway server daemon is available on port."""
async def check_hermes_server_active(port: int = 8642) -> tuple[bool, str]:
"""Check if Hermes gateway server daemon is responding to health requests."""
url = f"http://localhost:{port}/api/health"
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=2.0)) as session:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=1.5)) as session:
async with session.get(url) as resp:
if resp.status == 200:
return True, f"Hermes server active on http://localhost:{port}"
except Exception:
pass
return False, "Hermes server daemon not active"
async def ensure_hermes_server(port: int = 8642) -> tuple[bool, str]:
"""Ensure Hermes gateway server daemon or CLI binary is available."""
active, msg = await check_hermes_server_active(port)
if active:
return True, msg
cli = find_hermes_cli()
if not cli:
@@ -146,6 +154,7 @@ class HermesLLM(FrameProcessor):
self._cli_path = find_hermes_cli() or "hermes"
self._use_server = use_server
self._session_renamed = False
self._http_session: aiohttp.ClientSession | None = None
# Keep the conversation lineage with the workspace. A single global
# session file can make two voice-agent workspaces resume each other's
@@ -157,6 +166,37 @@ class HermesLLM(FrameProcessor):
if self._session_id:
logger.info(f"Loaded existing Hermes session ID: {self._session_id}")
def reset_session(self):
"""Reset active session so a fresh session starts on the next turn."""
logger.info("Resetting active Hermes session state...")
self._session_id = None
self._session_renamed = False
self._history.clear()
try:
if self._session_state_file.exists():
self._session_state_file.unlink()
except Exception as e:
logger.warning(f"Could not remove session file during reset: {e}")
def _sync_disk_session(self):
"""Sync in-memory session ID with disk file state prior to each turn."""
disk_sid = self._load_session_id()
if disk_sid != self._session_id:
logger.info(f"Hermes session state updated from disk: {self._session_id} -> {disk_sid}")
self._session_id = disk_sid
self._session_renamed = False
self._history.clear()
async def _get_http_session(self) -> aiohttp.ClientSession:
if self._http_session is None or self._http_session.closed:
self._http_session = aiohttp.ClientSession()
return self._http_session
async def _close_http_session(self):
if self._http_session and not self._http_session.closed:
await self._http_session.close()
self._http_session = None
def _load_session_id(self) -> str | None:
if self._session_state_file.exists():
try:
@@ -219,6 +259,7 @@ class HermesLLM(FrameProcessor):
logger.info(f"Hermes LLM engine initialized: {reason}")
elif isinstance(frame, (EndFrame, CancelFrame)):
await self._cancel_turn()
await self._close_http_session()
await self.push_frame(frame, direction)
elif isinstance(frame, InterruptionFrame):
await self._cancel_turn()
@@ -289,6 +330,7 @@ class HermesLLM(FrameProcessor):
async def _run_turn(self, utterance: str):
self._sync_disk_model()
self._sync_disk_session()
self._history.append({"role": "user", "content": utterance})
await self.push_frame(LLMFullResponseStartFrame())
@@ -313,7 +355,7 @@ class HermesLLM(FrameProcessor):
async def _run_turn_server(self, utterance: str, chunks: list[str]):
"""Run turn via Hermes Server / Gateway HTTP API if available."""
try:
ok, _ = await ensure_hermes_server(self._port)
ok, _ = await check_hermes_server_active(self._port)
if not ok:
raise RuntimeError("Hermes server daemon unavailable")
@@ -325,8 +367,8 @@ class HermesLLM(FrameProcessor):
if self._model and self._model.lower() not in ("default", "none", ""):
payload["model"] = self._model
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as resp:
session = await self._get_http_session()
async with session.post(url, json=payload) as resp:
if resp.status == 200:
data = await resp.json()
text_val = data.get("reply") or data.get("text") or data.get("content", "")