fix(multiplex): served profiles read their own sessions.* settings
gateway/run.py bridges the LAUNCH profile's sessions.cjk_fts / search_slow_ms into HERMES_CJK_FTS / HERMES_SEARCH_SLOW_MS at import (and re-bridged them per turn), and hermes_state_fts / hermes_state_search read os.getenv — so a served secondary always got the default profile's values. hermes_state_common.routed_sessions_setting() reads the routed profile's config.yaml under a HERMES_HOME override and the env bridge when unscoped; both consumers use it. The per-turn re-bridge is skipped inside a secondary's scope so it can no longer write the default's slots from a routed turn.
This commit is contained in:
+10
-6
@@ -1605,7 +1605,12 @@ def _reload_runtime_env_preserving_config_authority() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _bridge_max_turns_from_config(home: "Path") -> None:
|
def _bridge_max_turns_from_config(home: "Path") -> None:
|
||||||
"""Re-bridge agent.max_turns (+ sessions.*) per turn; managed overlay applies or it reverts."""
|
"""Re-bridge agent.max_turns (+ sessions.*) per turn; managed overlay applies or it reverts.
|
||||||
|
Skipped inside a served secondary's scope: the env slots are the launch profile's and
|
||||||
|
hermes_state reads the routed profile's ``sessions.*`` from its own config under scope."""
|
||||||
|
from gateway.platforms._shared import profile_scoped
|
||||||
|
if profile_scoped():
|
||||||
|
return
|
||||||
config_path = home / 'config.yaml'
|
config_path = home / 'config.yaml'
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
return
|
return
|
||||||
@@ -1658,15 +1663,14 @@ class HygieneTurnHoldExceeded(Exception):
|
|||||||
def _multiplex_profile_homes(config: object) -> list[tuple[str, "Path"]]:
|
def _multiplex_profile_homes(config: object) -> list[tuple[str, "Path"]]:
|
||||||
"""Return the authoritative profile set for one multiplex gateway config."""
|
"""Return the authoritative profile set for one multiplex gateway config."""
|
||||||
from hermes_cli.profiles import profiles_to_serve
|
from hermes_cli.profiles import profiles_to_serve
|
||||||
return list(profiles_to_serve(
|
return list(profiles_to_serve(multiplex=True))
|
||||||
multiplex=True, profile_allowlist=getattr(config, "multiplex_profile_allowlist", None)))
|
|
||||||
|
|
||||||
|
|
||||||
def _cron_tick_profile_homes(config: object) -> list[tuple[str, "Path"]]:
|
def _cron_tick_profile_homes(config: object) -> list[tuple[str, "Path"]]:
|
||||||
"""Profile homes the in-process ticker visits under multiplex: the served set PLUS the
|
"""Profile homes the in-process ticker visits under multiplex: the served set PLUS the
|
||||||
process-active profile. ``profiles_to_serve`` starts at default + allowlist, so a
|
process-active profile: ``profiles_to_serve`` lists default + every live named profile, but a
|
||||||
``--profile <name>`` multiplexer was omitted unless allowlisted — and allowlisting it would
|
``--profile <name>`` multiplexer's own profile may sit outside ``profiles/`` (custom
|
||||||
start a second adapter on its own bot token. Adapter startup already skips ``active``."""
|
HERMES_HOME). Adapter startup already skips ``active``."""
|
||||||
from hermes_cli.profiles import get_active_profile_name, get_profile_dir
|
from hermes_cli.profiles import get_active_profile_name, get_profile_dir
|
||||||
|
|
||||||
homes = _multiplex_profile_homes(config)
|
homes = _multiplex_profile_homes(config)
|
||||||
|
|||||||
@@ -23,6 +23,26 @@ _PREVIEW_SCAFFOLD_WINDOW = 400
|
|||||||
_PREVIEW_MAX_CHARS = 60
|
_PREVIEW_MAX_CHARS = 60
|
||||||
|
|
||||||
|
|
||||||
|
def routed_sessions_setting(key: str, env_var: str) -> Any:
|
||||||
|
"""``sessions.<key>`` for the profile whose state.db this process is touching.
|
||||||
|
|
||||||
|
``gateway/run.py`` bridges the LAUNCH profile's ``sessions.*`` into ``env_var`` (the cross-process
|
||||||
|
carrier CLI/cron children read). Under a multiplexer a routed turn runs with a HERMES_HOME override
|
||||||
|
and that env slot holds the default profile's value, so a served profile with different
|
||||||
|
``sessions.*`` settings must read its own config.yaml. Unscoped: the env bridge, as before.
|
||||||
|
Returns ``None`` when neither source sets the key.
|
||||||
|
"""
|
||||||
|
from hermes_constants import get_hermes_home_override
|
||||||
|
|
||||||
|
if get_hermes_home_override():
|
||||||
|
try:
|
||||||
|
from hermes_cli.config import load_config_readonly
|
||||||
|
return (load_config_readonly().get("sessions") or {}).get(key)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return os.environ.get(env_var)
|
||||||
|
|
||||||
|
|
||||||
def escape_like(text: str) -> str:
|
def escape_like(text: str) -> str:
|
||||||
"""Escape LIKE wildcards (``%``, ``_``) so derived text matches literally; pair with ``ESCAPE '\\'``.
|
"""Escape LIKE wildcards (``%``, ``_``) so derived text matches literally; pair with ``ESCAPE '\\'``.
|
||||||
``_`` is common in branch names/titles/paths and a substring match must not silently widen."""
|
``_`` is common in branch names/titles/paths and a substring match must not silently widen."""
|
||||||
|
|||||||
+5
-3
@@ -9,7 +9,8 @@ from pathlib import Path
|
|||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
from hermes_state_common import FTS_CJK_STALE_KEY, FTS_STALE_KEY, _FTS_CJK_TRIGGERS, _FTS_TRIGGERS
|
from hermes_state_common import (FTS_CJK_STALE_KEY, FTS_STALE_KEY, _FTS_CJK_TRIGGERS, _FTS_TRIGGERS,
|
||||||
|
routed_sessions_setting)
|
||||||
from hermes_state_errors import is_fts_scoped_corruption_error
|
from hermes_state_errors import is_fts_scoped_corruption_error
|
||||||
|
|
||||||
# caplog tests pin the "hermes_state" logger name.
|
# caplog tests pin the "hermes_state" logger name.
|
||||||
@@ -102,8 +103,9 @@ def fts5_cjk_so_path() -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _cjk_fts_config_enabled() -> bool:
|
def _cjk_fts_config_enabled() -> bool:
|
||||||
"""config.yaml ``sessions.cjk_fts`` (default on), via its env bridge."""
|
"""config.yaml ``sessions.cjk_fts`` (default on) for the profile being served."""
|
||||||
return os.getenv("HERMES_CJK_FTS", "1").strip().lower() not in ("0", "false", "off", "no")
|
value = routed_sessions_setting("cjk_fts", "HERMES_CJK_FTS")
|
||||||
|
return value is None or str(value).strip().lower() not in ("0", "false", "off", "no")
|
||||||
|
|
||||||
|
|
||||||
def load_fts5_cjk_extension(conn: sqlite3.Connection) -> bool:
|
def load_fts5_cjk_extension(conn: sqlite3.Connection) -> bool:
|
||||||
|
|||||||
+11
-3
@@ -12,17 +12,25 @@ import time
|
|||||||
from typing import Any, Callable, Collection, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Collection, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from agent.skill_commands import describe_skill_invocation
|
from agent.skill_commands import describe_skill_invocation
|
||||||
from utils import env_float
|
|
||||||
from hermes_state_common import (
|
from hermes_state_common import (
|
||||||
FTS_CJK_STALE_KEY, FTS_SQL, FTS_STALE_KEY, FTS_STORAGE_VERSION, FTS_TOOL_CONTENT_PREFIX_CHARS,
|
FTS_CJK_STALE_KEY, FTS_SQL, FTS_STALE_KEY, FTS_STORAGE_VERSION, FTS_TOOL_CONTENT_PREFIX_CHARS,
|
||||||
FTS_TOOL_FULL_CONTENT_HIGH_WATER_KEY, FTS_TRIGRAM_EXCLUDED_SOURCES, FTS_TRIGRAM_SQL,
|
FTS_TOOL_FULL_CONTENT_HIGH_WATER_KEY, FTS_TRIGRAM_EXCLUDED_SOURCES, FTS_TRIGRAM_SQL,
|
||||||
MAX_FTS5_QUERY_CHARS, SCHEMA_VERSION, _FTS_CJK_TRIGGERS,
|
MAX_FTS5_QUERY_CHARS, SCHEMA_VERSION, _FTS_CJK_TRIGGERS,
|
||||||
escape_like as _escape_like, fts_rebuild_admission, fts_trigram_session_sql,
|
escape_like as _escape_like, fts_rebuild_admission, fts_trigram_session_sql, routed_sessions_setting,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-split logger identity so log filtering/capture is unchanged.
|
# Pre-split logger identity so log filtering/capture is unchanged.
|
||||||
logger = logging.getLogger("hermes_state")
|
logger = logging.getLogger("hermes_state")
|
||||||
|
|
||||||
|
|
||||||
|
def _search_slow_ms() -> float:
|
||||||
|
"""``sessions.search_slow_ms`` for the served profile (default 1000; 0 logs every call)."""
|
||||||
|
value = routed_sessions_setting("search_slow_ms", "HERMES_SEARCH_SLOW_MS")
|
||||||
|
try:
|
||||||
|
return 1000.0 if value is None or str(value).strip() == "" else float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 1000.0
|
||||||
|
|
||||||
# Characters FTS5's query grammar rejects outside a quoted phrase (anything missing
|
# Characters FTS5's query grammar rejects outside a quoted phrase (anything missing
|
||||||
# reaches MATCH raw and raises -> zero results). ``%`` is deliberately excluded: the
|
# reaches MATCH raw and raises -> zero results). ``%`` is deliberately excluded: the
|
||||||
# CJK LIKE fallback needs it literal (that path escapes wildcards itself).
|
# CJK LIKE fallback needs it literal (that path escapes wildcards itself).
|
||||||
@@ -1015,7 +1023,7 @@ class SessionSearchMixin:
|
|||||||
return rows
|
return rows
|
||||||
finally:
|
finally:
|
||||||
elapsed_ms = (time.time() - started) * 1000.0
|
elapsed_ms = (time.time() - started) * 1000.0
|
||||||
if elapsed_ms >= env_float("HERMES_SEARCH_SLOW_MS", 1000.0):
|
if elapsed_ms >= _search_slow_ms():
|
||||||
logger.info("slow session search: path=%s elapsed=%.0fms rows=%s query=%r",
|
logger.info("slow session search: path=%s elapsed=%.0fms rows=%s query=%r",
|
||||||
self._describe_search_path(query), elapsed_ms, len(rows) if rows is not None else "err",
|
self._describe_search_path(query), elapsed_ms, len(rows) if rows is not None else "err",
|
||||||
query[: 200])
|
query[: 200])
|
||||||
|
|||||||
Reference in New Issue
Block a user