diff --git a/cron/scheduler.py b/cron/scheduler.py index c49370352c..c17c1fa46f 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -114,12 +114,20 @@ from cron.jobs import get_due_jobs, mark_job_run, save_job_output, advance_next_ # locally for audit. SILENT_MARKER = "[SILENT]" -# Resolve Hermes home directory (respects HERMES_HOME override) -_hermes_home = get_hermes_home() +# Backward-compatible module override used by tests and emergency monkeypatches. +_hermes_home: Path | None = None -# File-based lock prevents concurrent ticks from gateway + daemon + systemd timer -_LOCK_DIR = _hermes_home / "cron" -_LOCK_FILE = _LOCK_DIR / ".tick.lock" + +def _get_hermes_home() -> Path: + """Resolve Hermes home dynamically while preserving test monkeypatch hooks.""" + return _hermes_home or get_hermes_home() + + +def _get_lock_paths() -> tuple[Path, Path]: + """Resolve cron lock paths at call time so profile/env changes are honored.""" + hermes_home = _get_hermes_home() + lock_dir = hermes_home / "cron" + return lock_dir, lock_dir / ".tick.lock" def _resolve_origin(job: dict) -> Optional[dict]: @@ -597,7 +605,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]: """ from hermes_constants import get_hermes_home - scripts_dir = get_hermes_home() / "scripts" + scripts_dir = _get_hermes_home() / "scripts" scripts_dir.mkdir(parents=True, exist_ok=True) scripts_dir_resolved = scripts_dir.resolve() @@ -1058,9 +1066,9 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: # changes take effect without a gateway restart. from dotenv import load_dotenv try: - load_dotenv(str(_hermes_home / ".env"), override=True, encoding="utf-8") + load_dotenv(str(_get_hermes_home() / ".env"), override=True, encoding="utf-8") except UnicodeDecodeError: - load_dotenv(str(_hermes_home / ".env"), override=True, encoding="latin-1") + load_dotenv(str(_get_hermes_home() / ".env"), override=True, encoding="latin-1") delivery_target = _resolve_delivery_target(job) if delivery_target: @@ -1078,7 +1086,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: _cfg = {} try: import yaml - _cfg_path = str(_hermes_home / "config.yaml") + _cfg_path = str(_get_hermes_home() / "config.yaml") if os.path.exists(_cfg_path): with open(_cfg_path) as _f: _cfg = yaml.safe_load(_f) or {} @@ -1112,7 +1120,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: if prefill_file: pfpath = Path(prefill_file).expanduser() if not pfpath.is_absolute(): - pfpath = _hermes_home / pfpath + pfpath = _get_hermes_home() / pfpath if pfpath.exists(): try: with open(pfpath, "r", encoding="utf-8") as _pf: @@ -1436,12 +1444,13 @@ def tick(verbose: bool = True, adapters=None, loop=None) -> int: Returns: Number of jobs executed (0 if another tick is already running) """ - _LOCK_DIR.mkdir(parents=True, exist_ok=True) + lock_dir, lock_file = _get_lock_paths() + lock_dir.mkdir(parents=True, exist_ok=True) # Cross-platform file locking: fcntl on Unix, msvcrt on Windows lock_fd = None try: - lock_fd = open(_LOCK_FILE, "w") + lock_fd = open(lock_file, "w") if fcntl: fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) elif msvcrt: diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 0c362a400c..ddf98e7830 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -4090,7 +4090,14 @@ class FeishuAdapter(BasePlatformAdapter): content=payload, uuid_value=str(uuid.uuid4()), ) - request = self._build_create_message_request("chat_id", body) + # Detect whether chat_id is an open_id (private message) or a group chat_id. + # Feishu API requires receive_id_type="open_id" for user DMs (oc_/ou_ prefix) + # and receive_id_type="chat_id" for group chats (ch_ prefix). + if chat_id.startswith(("oc_", "ou_")): + receive_id_type = "open_id" + else: + receive_id_type = "chat_id" + request = self._build_create_message_request(receive_id_type, body) return await asyncio.to_thread(self._client.im.v1.message.create, request) @staticmethod