diff --git a/gateway/shutdown_watchdog.py b/gateway/shutdown_watchdog.py index 66d1e4d118..528c8e8a01 100644 --- a/gateway/shutdown_watchdog.py +++ b/gateway/shutdown_watchdog.py @@ -295,11 +295,14 @@ async def _tick_socket_handler(reader: asyncio.StreamReader, writer: asyncio.Str """Answer a liveness ping with one byte; never raises. Runs on the gateway loop, so a reply witnesses loop schedulability; the write is a socket-buffer copy (no fsync), immune to the stalls that age the heartbeat.""" - with contextlib.suppress(Exception): + try: writer.write(b"1") await writer.drain() - with contextlib.suppress(Exception): - writer.close() + except Exception: + pass + finally: # close even on CancelledError (BaseException), as on BASE + with contextlib.suppress(Exception): + writer.close() def _sweep_stale_tick_sockets(own_path: Path) -> None: diff --git a/tools/skill_usage.py b/tools/skill_usage.py index bd3f582908..ebf8128037 100644 --- a/tools/skill_usage.py +++ b/tools/skill_usage.py @@ -137,25 +137,31 @@ def _read_hub_installed_names() -> Set[str]: """Hub-installed names (``.hub/lock.json``) plus the frontmatter name of each in-tree ``install_path``.""" skills_dir = _skills_dir() lock_path = skills_dir / ".hub" / "lock.json" + if not lock_path.exists(): + return set() + # The whole walk sits under one handler (BASE semantics): an OSError anywhere — including the + # per-skill SKILL.md read — logs and yields an empty set rather than a partial one. try: # errors="replace": hub descriptions can carry Windows-1252 high bytes; a strict read raises # UnicodeDecodeError (a ValueError, not caught below) and would 500 the whole /api/skills endpoint. - data = json.loads(lock_path.read_text(encoding="utf-8", errors="replace")) if lock_path.exists() else {} - except (OSError, json.JSONDecodeError) as e: - logger.debug("Failed to read hub lock file: %s", e) - return set() - installed = (data.get("installed") or {}) if isinstance(data, dict) else None - if not isinstance(installed, dict): - return set() - names = {str(k) for k in installed} - paths = (e.get("install_path") for e in installed.values() if isinstance(e, dict)) - for install_path in (p for p in paths if isinstance(p, str) and p.strip()): - with suppress(OSError, ValueError): # ValueError: install_path escapes the skills dir - resolved = (skills_dir / install_path).resolve() - resolved.relative_to(skills_dir.resolve()) + data = json.loads(lock_path.read_text(encoding="utf-8", errors="replace")) + installed = (data.get("installed") or {}) if isinstance(data, dict) else None + if not isinstance(installed, dict): + return set() + names = {str(k) for k in installed} + paths = (e.get("install_path") for e in installed.values() if isinstance(e, dict)) + for install_path in (p for p in paths if isinstance(p, str) and p.strip()): + try: # ValueError: install_path escapes the skills dir + resolved = (skills_dir / install_path).resolve() + resolved.relative_to(skills_dir.resolve()) + except (OSError, ValueError): + continue if (resolved / "SKILL.md").exists(): names.add(_read_skill_name(resolved / "SKILL.md", fallback=resolved.name)) - return names + return names + except (OSError, json.JSONDecodeError) as e: + logger.debug("Failed to read hub lock file: %s", e) + return set() def _prune_builtins_enabled() -> bool: diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index 6759d8a12e..b8374d15e8 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -301,8 +301,8 @@ def _(rid, params: dict) -> dict: parent_session_id = _str_param(params, "parent_session_id") or None # Only an explicitly chosen existing workspace persists as cwd; the launch-dir fallback is "No workspace". explicit_cwd = False + raw_cwd = _str_param(params, "cwd") # unguarded, as on BASE: only the path check is best-effort with contextlib.suppress(Exception): - raw_cwd = _str_param(params, "cwd") explicit_cwd = bool(raw_cwd) and os.path.isdir(os.path.abspath(os.path.expanduser(raw_cwd))) _enable_gateway_prompts() # ``profile`` (app-global remote mode): stored so the build and every turn re-bind HERMES_HOME.