fix(memory/hindsight): resolve retain shaping through the profile scope, never os.environ
_load_config() reads the Hindsight bank, mode and retain tags through the profile
secret scope, but _apply_retain_settings() then discarded that answer and re-read
os.environ whenever the config value was falsy:
return cfg.get(key) or os.environ.get(env_var, default)
Under gateway.multiplex_profiles os.environ holds the DEFAULT profile's .env, so a
secondary profile's scoped miss came back as the default profile's retain tags,
observation scopes, source and speaker prefixes — the fallback-after-miss shape
gateway/AGENTS.md forbids. Tags are Hindsight's retrieval partition and
metadata.source is opt-in by design, so the secondary's memories were both
mislabelled and selectable by the default profile's tag filters.
Both halves now go through _scoped_setting(), which resolves the value with
get_secret() and falls back to the provider's OWN default — a miss is a miss, the
same rule embedded.py already applies to the daemon's key and base URL. The three
raw reads left inside _load_config() (retain_source, retain_user_prefix,
retain_assistant_prefix), directly under the comment declaring them per-profile,
go through it too.
Single-profile deployments are unchanged: with no scope installed get_secret()
still reads the process env, where the value IS this profile's own.
Fixes #108865
This commit is contained in:
committed by
Teknium
parent
122ad719b5
commit
a48dde5316
@@ -25,7 +25,7 @@ from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from agent.memory_provider import MemoryProvider, RecallStatus, spawn_context_thread
|
||||
from agent.secret_scope import get_secret
|
||||
from agent.secret_scope import UnscopedSecretError, get_secret
|
||||
from hermes_cli.config import cfg_get
|
||||
from hermes_constants import get_hermes_home
|
||||
from hermes_time import now as _hermes_now
|
||||
@@ -63,6 +63,21 @@ def _ensure_client_dependency() -> None:
|
||||
raise ImportError(str(exc)) from exc
|
||||
|
||||
|
||||
def _scoped_setting(name: str, default: str = "") -> str:
|
||||
"""Profile-scoped read of a per-profile Hindsight setting, with the provider's own default on a miss.
|
||||
|
||||
Under ``gateway.multiplex_profiles`` ``os.environ`` holds the DEFAULT profile's ``.env``, so a miss
|
||||
is a miss — never ``os.environ`` (same rule as the daemon's key and base URL in ``embedded.py``).
|
||||
Single-profile deployments are unchanged: with no scope installed ``get_secret`` still reads the
|
||||
process env, where the value IS this profile's own.
|
||||
"""
|
||||
try:
|
||||
value = get_secret(name, default)
|
||||
except UnscopedSecretError:
|
||||
return default
|
||||
return default if value is None else value
|
||||
|
||||
|
||||
def _cloud_api_key(config: dict) -> str:
|
||||
return config.get("apiKey") or config.get("api_key") or get_secret("HINDSIGHT_API_KEY", "")
|
||||
|
||||
@@ -248,9 +263,9 @@ def _load_config() -> dict:
|
||||
"idle_timeout": _parse_int_setting(os.environ.get("HINDSIGHT_IDLE_TIMEOUT"), _DEFAULT_IDLE_TIMEOUT),
|
||||
"retain_tags": get_secret("HINDSIGHT_RETAIN_TAGS", "") or "",
|
||||
"observation_scopes": get_secret("HINDSIGHT_RETAIN_OBSERVATION_SCOPES", "") or "",
|
||||
"retain_source": os.environ.get("HINDSIGHT_RETAIN_SOURCE", _DEFAULT_RETAIN_SOURCE),
|
||||
"retain_user_prefix": os.environ.get("HINDSIGHT_RETAIN_USER_PREFIX", "User"),
|
||||
"retain_assistant_prefix": os.environ.get("HINDSIGHT_RETAIN_ASSISTANT_PREFIX", "Assistant"),
|
||||
"retain_source": _scoped_setting("HINDSIGHT_RETAIN_SOURCE", _DEFAULT_RETAIN_SOURCE),
|
||||
"retain_user_prefix": _scoped_setting("HINDSIGHT_RETAIN_USER_PREFIX", "User"),
|
||||
"retain_assistant_prefix": _scoped_setting("HINDSIGHT_RETAIN_ASSISTANT_PREFIX", "Assistant"),
|
||||
"banks": {"hermes": {"bankId": get_secret("HINDSIGHT_BANK_ID", "") or "hermes",
|
||||
"budget": os.environ.get("HINDSIGHT_BUDGET", "mid"), "enabled": True}},
|
||||
}
|
||||
@@ -724,7 +739,9 @@ class HindsightMemoryProvider(MemoryProvider):
|
||||
|
||||
def _apply_retain_settings(self, cfg: dict) -> None:
|
||||
def _cfg_or_env(key: str, env_var: str, default: str = "") -> Any:
|
||||
return cfg.get(key) or os.environ.get(env_var, default)
|
||||
# The env half is the same per-profile value ``_load_config`` resolves through the scope;
|
||||
# a raw read here handed a multiplexed secondary the DEFAULT profile's retain shaping back.
|
||||
return cfg.get(key) or _scoped_setting(env_var, default)
|
||||
|
||||
self._retain_tags = _normalize_retain_tags(_cfg_or_env("retain_tags", "HINDSIGHT_RETAIN_TAGS"))
|
||||
self._tags = self._retain_tags or None
|
||||
|
||||
Reference in New Issue
Block a user