Skip to content

fix(gateway,tools): multiplexed profiles keep their own max_turns, fallback chain, hooks, auth, caches and paths (salvage #56315 #56508 #63962, fixes #95685) - #108453

Merged
teknium1 merged 7 commits into
mainfrom
fix/mux-process-memo
Sep 11, 2026
Merged

teknium1 merged 7 commits into
mainfrom
fix/mux-process-memo

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Under gateway.multiplex_profiles, a secondary profile's turns now run with its own agent.max_turns, fallback chain, gateway hooks, Nous auth.json, media-delivery policy, checkpoint/snapshot stores, tool limits, browser timeouts, sandbox timezone and tool-schema paths — instead of the launch profile's values frozen into module constants and process caches at import.

Changes

Gateway / agent side (fix(gateway))

  • gateway/run.py::_current_max_iterations — a routed turn (HERMES_HOME override) reads agent.max_turns from its own config instead of the process-wide HERMES_MAX_ITERATIONS bridge filled from _hermes_home.
  • gateway/run_config_loaders.py::_refresh_fallback_model — reads the active gateway home and keeps a last-known-good chain per home (was one runner-wide slot from the launch home); _load_prefill_messages relative paths likewise.
  • agent/auxiliary_client.py — _AUTH_JSON_PATH resolved per call via hermes_cli.auth._auth_file_path(); a secondary's compression/title/vision calls stop authenticating with the default profile's Nous token.
  • gateway/hooks.py — HOOKS_DIR resolved per call (salvaged from security(gateway): re-resolve hooks directory per call to fix profile isolation #56508, @srojk34's commit kept) and a new ProfileHookRegistries holds one HookRegistry per served home, picked from the active scope at emit time. Secondaries' hooks/ now fire; the default's handlers no longer see other profiles' messages/responses/user ids.
  • agent/shell_hooks.py::_spawn — child env via build_subprocess_env(scrub_secrets=is_multiplex_active()): routed HERMES_HOME, default-profile secrets scrubbed under multiplexing; stdin payload gains profile. Single-profile runs keep the process env byte-for-byte.
  • gateway/media_policy.py (+ platforms/base.py, media_fetch.py) — media_delivery_strict/allow_dirs/trust_recent* read the routed profile's config under an override; the env bridge remains the contract when no override is active.

Tools side (fix(tools), + cherry-picked #56315 by @srojk34)

  • tools/process_registry.py::_checkpoint_path, tools/checkpoint_manager.py::_resolve_checkpoint_base, gateway/sticker_cache.py::_resolve_cache_path, modal/singularity _snapshot_store() — resolved per call; existing monkeypatch.setattr(<CONSTANT>) test seams preserved (*_AT_IMPORT pattern from tools/skills_tool._skills_dir).
  • plugins/platforms/feishu/feishu_comment_rules.py — _MtimeCache is path-keyed with invalidate(); _rules_file()/_pairing_file() follow the routed profile (second half of fix(feishu): preserve profile scope for SDK-thread callbacks #63962, credit @nateEc).
  • tools/tool_output_limits.py, tools/browser_tool.py, tools/browser_camofox.py — caches keyed by hermes_home_key(); tools/file_tools.py drops its private file_read_max_chars memo for the already path+mtime-cached load_config_readonly().
  • hermes_time.py::get_timezone_name — under multiplexing the routed profile's timezone beats the env HERMES_TIMEZONE bridged from the default; both execute_code sandbox TZ sites use it.
  • tools/cronjob_tools.py, tools/tts_tool.py, tools/skill_manager_tool.py — static schema text is profile-neutral; dynamic_schema_overrides rebuilds the display_hermes_home() path per get_definitions() ([Bug]: Tool schema f-strings call display_hermes_home() at import time, freezing a stale profile path into tool descriptions #95685).

Docs: multi-profile-gateways.md (what follows the routed profile) and hooks.md (profile payload field).

Validation

Live repro (/tmp/mux_audit/fix-process-memo/repro.py: temp HERMES_HOME A + profiles/B with different values everywhere; warm every cache under A, then set_hermes_home_override(B)):

check (B's turn) before after
_current_max_iterations() 7 (A) 99
_refresh_fallback_model() A/fallback B/fallback
aux _read_nous_auth() token TOKEN_A TOKEN_B
gateway hooks loaded hook_A hook_B
processes.json / checkpoints/ / snapshot stores A/… profiles/B/…
feishu rules enabled A's B's
tool_output.max_bytes / file_read_max_chars 222 / 111 888 / 999
browser / camofox command_timeout 33 77
sandbox TZ / hermes_time America/New_York Asia/Tokyo
media strict / allow dirs A's B's
cronjob / tts / skill_manage schema path A's home profiles/B

21 FAIL → 0 FAIL.

Tests: tests/gateway/test_multiplex_process_memo_scope.py (4, red on base), tests/agent/test_shell_hooks.py::TestRoutedProfileEnv, tests/tools/test_multiplex_tool_memo_scope.py (3), feishu/timezone invariants, salvaged tests/test_profile_isolation_runtime.py classes trimmed to ≤2 each. scripts/run_tests.sh tests/gateway tests/agent tests/tools tests/plugins: all green except pre-existing failures also red on origin/main in this environment (test_modal_snapshot_isolation ×2 — lazy-install gate, test_web_tools_config ×2, test_execution_flag_detection ×1) and one unrelated retry-flake (test_compression_stall_fallback_78981).

Root cause: the multiplexer runs every profile in one process whose module-level get_hermes_home() constants, os.environ bridges and unkeyed caches were populated once from the launch profile.

Audit refs: /tmp/mux_audit/audit-memo F3, F4, F6 (auth.json half), F7, F9, F10, F12; audit-startup F9.

Credits: @srojk34 — earliest fixes for hooks dir (#56508) and checkpoint/sticker paths (#56315), both cherry-picked; @nateEc (Nathan Shan) — Feishu comment-rules half of #63962, co-authored; @rawnets — reporter of #95685.

Fixes #95685. Addresses #56315, #56508, #63962.

Infographic

One gateway, many profiles

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on e10797d — ci: retrigger (zero-job dispatch on 155683e)

⚠️ Warnings

OSV vulnerability scan · View job

80 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 6m5s vs 6m23s (-4.7%). 5 job(s) slower, 7 faster, 3 unchanged.

  • Docs Site / docs-site-checks: -33.0s
  • Python tests / Run tests: +23.0s
  • OS-specific tests / Windows-only tests: -20.0s
  • OS-specific tests / macOS-only tests: +9.0s
  • Python tests / e2e: -6.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tools Tool registry, model_tools, toolsets area/profiles Multi-profile isolation, HERMES_HOME scoping area/auth Authentication, OAuth, credential pools sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 11, 2026
@teknium1 teknium1 closed this Sep 11, 2026
@teknium1 teknium1 reopened this Sep 11, 2026
@teknium1
teknium1 force-pushed the fix/mux-process-memo branch from 67a8092 to 7096421 Compare September 11, 2026 21:10
srojk34 and others added 7 commits September 11, 2026 15:35
… isolation

gateway/hooks.py::HOOKS_DIR is resolved once at import time via
get_hermes_home(), which is a context-local ContextVar under the
multiplexed gateway (multiple profiles sharing one process, each owning
its own Gateway/HookRegistry instance). Freezing the path at import time
pins every later HookRegistry.discover_and_load() call to whichever
profile's HERMES_HOME was active when this module was first imported --
so a later-starting profile silently discovers and executes the FIRST
profile's hook handlers (arbitrary Python code, not just data) against
its own live event context, including session_id/message/response text.
Same bug class already fixed for cache dirs, skills_hub, rich_sent_store,
and the OAuth/auth.json/sessions.json/checkpoint/sticker-cache paths.

Add a per-call resolver, following the established "respect an existing
test monkeypatch of the constant, otherwise re-resolve through
get_hermes_home()" pattern so the existing test seam in
tests/gateway/test_hooks.py keeps working unmodified.

(cherry picked from commit 1e4f96a)
…, hooks, aux auth and media policy

One multiplexed gateway process serves every profile, but several per-turn
reads still went through state frozen from the LAUNCH profile:

- `_current_max_iterations` re-bridged `agent.max_turns`/`sessions.*` from the
  module constant `_hermes_home` into one process-wide HERMES_MAX_ITERATIONS,
  so every secondary ran with the default profile's turn budget. A routed turn
  (HERMES_HOME override) now resolves `agent.max_turns` from its own config.
- `_refresh_fallback_model` read `_hermes_home/config.yaml` into one runner-wide
  slot, so secondaries fell back through the default's provider/model with their
  own keys. It now reads the active gateway home and keeps a last-known-good
  chain per home.
- `_load_prefill_messages` resolved relative paths against the launch home.
- `agent/auxiliary_client._AUTH_JSON_PATH` was an import-time constant, so a
  secondary's compression/title/vision calls authenticated to Nous with the
  default profile's token when it had no pool entry. Resolved per call via
  `hermes_cli.auth._auth_file_path()` (patched constant still wins in tests).
- `gateway/hooks.HOOKS_DIR` was frozen at import and one `HookRegistry` was
  loaded outside any profile scope, so secondaries' `hooks/` never ran and the
  default profile's handlers received every profile's messages, responses and
  user ids. `HOOKS_DIR` now resolves per call (salvaged from #56508) and the
  runner holds one registry per served home, picked from the active scope at
  emit time and front-loaded under each secondary's startup scope.
- Shell-hook subprocesses inherited the launch `os.environ` (default HERMES_HOME
  and the default profile's secrets). They now get the routed HERMES_HOME via
  `build_subprocess_env`, scrubbed under multiplexing, and the stdin payload
  carries `profile` so one script can tell which profile fired it.
- Media-delivery policy (`gateway.strict`, `media_delivery_allow_dirs`,
  `trust_recent_files*`) was bridged once into env at startup and read from env
  per delivery; under a HERMES_HOME override the validator now reads the routed
  profile's config. Single-profile runs keep the env-bridge contract.

Audit: /tmp/mux_audit F3, F4, F6 (auth.json half), F7, F12 (media). Live repro
(temp HERMES_HOME A with profiles/B): before, B saw max_iterations 7,
fallback A/fallback, TOKEN_A, A's hooks, strict=A; after, all B's values.
tools/checkpoint_manager.py's CHECKPOINT_BASE and gateway/sticker_cache.py's
CACHE_PATH are resolved once at import time via get_hermes_home(), which is
a context-local ContextVar under the multiplexed gateway (multiple profiles
sharing one process). Freezing the path at import time pins every later
checkpoint/cache read-write to whichever profile's HERMES_HOME was active
when the module was first imported -- the same bug class already fixed for
cache dirs, skills_hub, rich_sent_store, and (this session) the OAuth/auth.json/
sessions.json paths.

CheckpointManager is "owned by AIAgent" per-instance, but its methods read
the frozen module constant directly instead of taking the store root from
the instance, so a profile's CheckpointManager can read/write code-edit
checkpoints into a different profile's store.

Add a per-call resolver for each path, following the established "respect
an existing test monkeypatch of the constant, otherwise re-resolve through
get_hermes_home()" pattern so the extensive existing test seams in
tests/tools/test_checkpoint_manager.py and tests/gateway/test_sticker_cache.py
keep working unmodified.

(cherry picked from commit 03ae075)
(cherry picked from commit b850c4b18e2ae2158a97c6cb87bd2057918b8170)
… and schema paths under multiplex

Under `gateway.multiplex_profiles` one gateway process serves every profile
under ~/.hermes/profiles/NAME/; each routed turn runs with a context-local
HERMES_HOME override while `os.environ` still holds the DEFAULT profile's
values. Anything evaluated once at import, or memoised in a single unkeyed
module slot, therefore freezes the LAUNCH profile's value and leaks it into
every other profile's turns. This lands the tools-side half of that class:

- tools/process_registry.py, tools/environments/{modal,singularity}.py:
  `_checkpoint_path()` / `_snapshot_store()` resolve `get_hermes_home()` at
  call time (same seam as `tools/skills_tool._skills_dir`, so the existing
  `monkeypatch.setattr(CHECKPOINT_PATH)` test sites keep working). Completes
  the checkpoint_manager / sticker_cache half cherry-picked from #56315.
- plugins/platforms/feishu/feishu_comment_rules.py: `_MtimeCache` is now
  path-keyed (accepts a Path or a zero-arg resolver, one (mtime, data) slot
  per resolved path) with `invalidate()`; `_rules_file()` / `_pairing_file()`
  resolve the routed profile's files. Proposed in #63962.
- tools/tool_output_limits.py, tools/browser_tool.py, tools/browser_camofox.py:
  the process-lifetime config caches are dicts keyed by `hermes_home_key()`;
  the `_X_resolved` flags and the lifecycle reset keep their shape.
  tools/file_tools.py drops its private `file_read_max_chars` memo and reads
  the already mtime+path-cached `load_config_readonly()`.
- hermes_time.py: `get_timezone_name()`; when `is_multiplex_active()` the
  env `HERMES_TIMEZONE` (bridged from the default profile's config at gateway
  startup) is ignored in favour of the routed profile's config.yaml. Both
  sandbox TZ sites (code_execution_env/_tool) now use it.
- tools/cronjob_tools.py, tools/tts_tool.py, tools/skill_manager_tool.py:
  the static schema text is profile-neutral and `dynamic_schema_overrides=`
  rebuilds the `display_hermes_home()` / create-dir hint per
  `get_definitions()`, so a routed profile's model sees its own paths.

Refs #95685.

Co-authored-by: Nathan Shan <nathanielcrush51@gmail.com>
(cherry picked from commit 6d3fc6b07b3155c6196b1fd61a829283f1d7855c)
@teknium1
teknium1 force-pushed the fix/mux-process-memo branch from 7096421 to e10797d Compare September 11, 2026 22:35
@teknium1
teknium1 merged commit bdb5bf9 into main Sep 11, 2026
37 checks passed
@teknium1
teknium1 deleted the fix/mux-process-memo branch September 11, 2026 22:44
teknium1 added a commit that referenced this pull request Sep 12, 2026
… unified init path

The unified runtime copied _init_registries_and_clocks and the cron bootstrap from a
pre-fix snapshot: HookRegistry() (one process-wide registry, so served secondaries'
hooks/ never load and fire the launch profile's handlers) and default_profile="default"
with _multiplex_profile_homes (a --profile <name> multiplexer's own jobs treated as a
secondary's). Restore ProfileHookRegistries and _cron_tick_profile_homes /
runner._primary_profile_name from main (#108453, #108428).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Tool schema f-strings call display_hermes_home() at import time, freezing a stale profile path into tool descriptions

3 participants