2026-02-22 20:44:15 -08:00
|
|
|
"""
|
|
|
|
|
Session mirroring for cross-platform message delivery.
|
|
|
|
|
|
|
|
|
|
When a message is sent to a platform (via send_message or cron delivery),
|
|
|
|
|
this module appends a "delivery-mirror" record to the target session's
|
|
|
|
|
transcript so the receiving-side agent has context about what was sent.
|
|
|
|
|
|
|
|
|
|
Standalone -- works from CLI, cron, and gateway contexts without needing
|
|
|
|
|
the full SessionStore machinery.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
fix(cli): respect HERMES_HOME in all remaining hardcoded ~/.hermes paths
Several files resolved paths via Path.home() / ".hermes" or
os.path.expanduser("~/.hermes/..."), bypassing the HERMES_HOME
environment variable. This broke isolation when running multiple
Hermes instances with distinct HERMES_HOME directories.
Replace all hardcoded paths with calls to get_hermes_home() from
hermes_cli.config, consistent with the rest of the codebase.
Files fixed:
- tools/process_registry.py (processes.json)
- gateway/pairing.py (pairing/)
- gateway/sticker_cache.py (sticker_cache.json)
- gateway/channel_directory.py (channel_directory.json, sessions.json)
- gateway/config.py (gateway.json, config.yaml, sessions_dir)
- gateway/mirror.py (sessions/)
- gateway/hooks.py (hooks/)
- gateway/platforms/base.py (image_cache/, audio_cache/, document_cache/)
- gateway/platforms/whatsapp.py (whatsapp/session)
- gateway/delivery.py (cron/output)
- agent/auxiliary_client.py (auth.json)
- agent/prompt_builder.py (SOUL.md)
- cli.py (config.yaml, images/, pastes/, history)
- run_agent.py (logs/)
- tools/environments/base.py (sandboxes/)
- tools/environments/modal.py (modal_snapshots.json)
- tools/environments/singularity.py (singularity_snapshots.json)
- tools/tts_tool.py (audio_cache)
- hermes_cli/status.py (cron/jobs.json, sessions.json)
- hermes_cli/gateway.py (logs/, whatsapp session)
- hermes_cli/main.py (whatsapp/session)
Tests updated to use HERMES_HOME env var instead of patching Path.home().
Closes #892
(cherry picked from commit 78ac1bba43b8b74a934c6172f2c29bb4d03164b9)
2026-03-11 07:31:41 +01:00
|
|
|
from hermes_cli.config import get_hermes_home
|
|
|
|
|
|
2026-02-22 20:44:15 -08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
fix(cli): respect HERMES_HOME in all remaining hardcoded ~/.hermes paths
Several files resolved paths via Path.home() / ".hermes" or
os.path.expanduser("~/.hermes/..."), bypassing the HERMES_HOME
environment variable. This broke isolation when running multiple
Hermes instances with distinct HERMES_HOME directories.
Replace all hardcoded paths with calls to get_hermes_home() from
hermes_cli.config, consistent with the rest of the codebase.
Files fixed:
- tools/process_registry.py (processes.json)
- gateway/pairing.py (pairing/)
- gateway/sticker_cache.py (sticker_cache.json)
- gateway/channel_directory.py (channel_directory.json, sessions.json)
- gateway/config.py (gateway.json, config.yaml, sessions_dir)
- gateway/mirror.py (sessions/)
- gateway/hooks.py (hooks/)
- gateway/platforms/base.py (image_cache/, audio_cache/, document_cache/)
- gateway/platforms/whatsapp.py (whatsapp/session)
- gateway/delivery.py (cron/output)
- agent/auxiliary_client.py (auth.json)
- agent/prompt_builder.py (SOUL.md)
- cli.py (config.yaml, images/, pastes/, history)
- run_agent.py (logs/)
- tools/environments/base.py (sandboxes/)
- tools/environments/modal.py (modal_snapshots.json)
- tools/environments/singularity.py (singularity_snapshots.json)
- tools/tts_tool.py (audio_cache)
- hermes_cli/status.py (cron/jobs.json, sessions.json)
- hermes_cli/gateway.py (logs/, whatsapp session)
- hermes_cli/main.py (whatsapp/session)
Tests updated to use HERMES_HOME env var instead of patching Path.home().
Closes #892
(cherry picked from commit 78ac1bba43b8b74a934c6172f2c29bb4d03164b9)
2026-03-11 07:31:41 +01:00
|
|
|
_SESSIONS_DIR = get_hermes_home() / "sessions"
|
2026-02-22 20:44:15 -08:00
|
|
|
_SESSIONS_INDEX = _SESSIONS_DIR / "sessions.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mirror_to_session(
|
|
|
|
|
platform: str,
|
|
|
|
|
chat_id: str,
|
|
|
|
|
message_text: str,
|
|
|
|
|
source_label: str = "cli",
|
2026-03-11 09:15:34 +01:00
|
|
|
thread_id: Optional[str] = None,
|
2026-02-22 20:44:15 -08:00
|
|
|
) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Append a delivery-mirror message to the target session's transcript.
|
|
|
|
|
|
|
|
|
|
Finds the gateway session that matches the given platform + chat_id,
|
|
|
|
|
then writes a mirror entry to both the JSONL transcript and SQLite DB.
|
|
|
|
|
|
|
|
|
|
Returns True if mirrored successfully, False if no matching session or error.
|
|
|
|
|
All errors are caught -- this is never fatal.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2026-03-11 09:15:34 +01:00
|
|
|
session_id = _find_session_id(platform, str(chat_id), thread_id=thread_id)
|
2026-02-22 20:44:15 -08:00
|
|
|
if not session_id:
|
2026-03-11 09:15:34 +01:00
|
|
|
logger.debug("Mirror: no session found for %s:%s:%s", platform, chat_id, thread_id)
|
2026-02-22 20:44:15 -08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
mirror_msg = {
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": message_text,
|
|
|
|
|
"timestamp": datetime.now().isoformat(),
|
|
|
|
|
"mirror": True,
|
|
|
|
|
"mirror_source": source_label,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_append_to_jsonl(session_id, mirror_msg)
|
|
|
|
|
_append_to_sqlite(session_id, mirror_msg)
|
|
|
|
|
|
|
|
|
|
logger.debug("Mirror: wrote to session %s (from %s)", session_id, source_label)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-03-11 09:15:34 +01:00
|
|
|
logger.debug("Mirror failed for %s:%s:%s: %s", platform, chat_id, thread_id, e)
|
2026-02-22 20:44:15 -08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 09:15:34 +01:00
|
|
|
def _find_session_id(platform: str, chat_id: str, thread_id: Optional[str] = None) -> Optional[str]:
|
2026-02-22 20:44:15 -08:00
|
|
|
"""
|
|
|
|
|
Find the active session_id for a platform + chat_id pair.
|
|
|
|
|
|
|
|
|
|
Scans sessions.json entries and matches where origin.chat_id == chat_id
|
|
|
|
|
on the right platform. DM session keys don't embed the chat_id
|
|
|
|
|
(e.g. "agent:main:telegram:dm"), so we check the origin dict.
|
|
|
|
|
"""
|
|
|
|
|
if not _SESSIONS_INDEX.exists():
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
try:
|
fix(gateway): add missing UTF-8 encoding to file I/O preventing crashes on Windows
On Windows, Python's open() defaults to the system locale encoding
(e.g. cp1254 for Turkish, cp1252 for Western European) instead of
UTF-8. The gateway already uses ensure_ascii=False in json.dumps()
to preserve Unicode characters in chat messages, but the
corresponding open() calls lack encoding="utf-8". This mismatch
causes UnicodeEncodeError / UnicodeDecodeError when users send
non-ASCII messages (Turkish, Japanese, Arabic, emoji, etc.) through
Telegram, Discord, WhatsApp, or Slack on Windows.
The project already fixed this for .env files in hermes_cli/config.py
(line 624) but the gateway module was missed.
Files fixed:
- gateway/session.py: session index + JSONL transcript read/write (5 calls)
- gateway/channel_directory.py: channel directory read/write (3 calls)
- gateway/mirror.py: session index read + transcript append (2 calls)
2026-03-04 11:32:57 +03:00
|
|
|
with open(_SESSIONS_INDEX, encoding="utf-8") as f:
|
2026-02-22 20:44:15 -08:00
|
|
|
data = json.load(f)
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
platform_lower = platform.lower()
|
|
|
|
|
best_match = None
|
|
|
|
|
best_updated = ""
|
|
|
|
|
|
|
|
|
|
for _key, entry in data.items():
|
|
|
|
|
origin = entry.get("origin") or {}
|
|
|
|
|
entry_platform = (origin.get("platform") or entry.get("platform", "")).lower()
|
|
|
|
|
|
|
|
|
|
if entry_platform != platform_lower:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
origin_chat_id = str(origin.get("chat_id", ""))
|
|
|
|
|
if origin_chat_id == str(chat_id):
|
2026-03-11 09:15:34 +01:00
|
|
|
origin_thread_id = origin.get("thread_id")
|
|
|
|
|
if thread_id is not None and str(origin_thread_id or "") != str(thread_id):
|
|
|
|
|
continue
|
2026-02-22 20:44:15 -08:00
|
|
|
updated = entry.get("updated_at", "")
|
|
|
|
|
if updated > best_updated:
|
|
|
|
|
best_updated = updated
|
|
|
|
|
best_match = entry.get("session_id")
|
|
|
|
|
|
|
|
|
|
return best_match
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _append_to_jsonl(session_id: str, message: dict) -> None:
|
|
|
|
|
"""Append a message to the JSONL transcript file."""
|
|
|
|
|
transcript_path = _SESSIONS_DIR / f"{session_id}.jsonl"
|
|
|
|
|
try:
|
fix(gateway): add missing UTF-8 encoding to file I/O preventing crashes on Windows
On Windows, Python's open() defaults to the system locale encoding
(e.g. cp1254 for Turkish, cp1252 for Western European) instead of
UTF-8. The gateway already uses ensure_ascii=False in json.dumps()
to preserve Unicode characters in chat messages, but the
corresponding open() calls lack encoding="utf-8". This mismatch
causes UnicodeEncodeError / UnicodeDecodeError when users send
non-ASCII messages (Turkish, Japanese, Arabic, emoji, etc.) through
Telegram, Discord, WhatsApp, or Slack on Windows.
The project already fixed this for .env files in hermes_cli/config.py
(line 624) but the gateway module was missed.
Files fixed:
- gateway/session.py: session index + JSONL transcript read/write (5 calls)
- gateway/channel_directory.py: channel directory read/write (3 calls)
- gateway/mirror.py: session index read + transcript append (2 calls)
2026-03-04 11:32:57 +03:00
|
|
|
with open(transcript_path, "a", encoding="utf-8") as f:
|
2026-02-22 20:44:15 -08:00
|
|
|
f.write(json.dumps(message, ensure_ascii=False) + "\n")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug("Mirror JSONL write failed: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _append_to_sqlite(session_id: str, message: dict) -> None:
|
|
|
|
|
"""Append a message to the SQLite session database."""
|
2026-03-07 04:24:45 +03:00
|
|
|
db = None
|
2026-02-22 20:44:15 -08:00
|
|
|
try:
|
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
|
db = SessionDB()
|
|
|
|
|
db.append_message(
|
|
|
|
|
session_id=session_id,
|
|
|
|
|
role=message.get("role", "assistant"),
|
|
|
|
|
content=message.get("content"),
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.debug("Mirror SQLite write failed: %s", e)
|
2026-03-07 04:24:45 +03:00
|
|
|
finally:
|
|
|
|
|
if db is not None:
|
|
|
|
|
db.close()
|