fix(gateway): preserve profile config changes during connection

This commit is contained in:
liuzikaii
2026-09-13 04:25:28 +08:00
committed by Teknium
parent 6de2dde61f
commit a15b8982d4
3 changed files with 81 additions and 1 deletions

View File

@@ -827,6 +827,7 @@ class GatewayAdapterLifecycleMixin:
Each profile connects under its own HERMES_HOME + secret scope; credential/listener collisions
are refused here — the only point seeing every profile's credentials together."""
from gateway.run import MultiplexConfigError, _multiplex_profile_homes
from gateway.run_profile_reconcile import profile_serve_signature
if not self._multiplex_on():
return 0
try:
@@ -837,9 +838,12 @@ class GatewayAdapterLifecycleMixin:
connected = 0
claimed = self._primary_resource_claims(active)
profile_homes = _multiplex_profile_homes(self.config)
self._served_profile_signatures = {}
for profile_name, profile_home in profile_homes:
if profile_name == active:
continue # handled by the primary startup loop
# Preserve changes made while the initial connection is awaiting I/O.
self._served_profile_signatures[profile_name] = profile_serve_signature(profile_home)
try:
connected += await self._start_one_profile_adapters(profile_name, profile_home, claimed)
except MultiplexConfigError:

View File

@@ -116,6 +116,9 @@ class GatewayProfileReconcileMixin:
result["removed"].append(name)
claimed = self._live_resource_claims(active)
for name in added + changed:
# Only acknowledge the configuration observed before connecting;
# a setup save during an awaited handshake needs another scan.
scan_signature = profile_serve_signature(current[name])
try:
connected = await self._start_one_profile_adapters(name, current[name], claimed)
except MultiplexConfigError as exc:
@@ -125,7 +128,7 @@ class GatewayProfileReconcileMixin:
except Exception:
logger.error("[MULTIPLEX] Failed to start adapters for profile '%s'", name, exc_info=True)
connected = 0
sigs[name] = profile_serve_signature(current[name])
sigs[name] = scan_signature
if name in added:
logger.info("[MULTIPLEX] Now serving profile '%s' (%s adapter(s) connected; %s)", name, connected, reason)
result["added"].append(name)

View File

@@ -0,0 +1,73 @@
"""A config saved while a profile connects must remain pending for the next scan."""
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from gateway.config import GatewayConfig, Platform
from gateway.run import GatewayRunner
@pytest.mark.asyncio
@pytest.mark.parametrize("startup", [False, True])
async def test_config_saved_during_connect_is_rescanned(tmp_path, monkeypatch, startup):
home = tmp_path / ".hermes"
profile = home / "profiles" / "worker"
profile.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(
"hermes_cli.profiles.get_active_profile_name", lambda: "default"
)
(profile / "config.yaml").write_text("model: {default: test}\n", encoding="utf-8")
secrets = profile / ".env"
secrets.write_text("DISCORD_BOT_TOKEN=discord-test\n", encoding="utf-8")
runner = object.__new__(GatewayRunner)
runner.config = GatewayConfig(multiplex_profiles=True)
runner._running = True
runner._primary_profile_name = "default"
(
runner.adapters,
runner._profile_adapters,
runner._failed_platforms,
runner._profile_failed_platforms,
) = {}, {}, {}, {}
runner.pairing_store, runner.pairing_stores = MagicMock(), {}
runner._busy_text_modes_by_profile, runner._busy_input_modes_by_profile = {}, {}
runner._register_config_hooks = lambda *a, **kw: None
runner._configure_profile_adapter = lambda *a: None
runner._sync_voice_mode_state_to_adapter = lambda *a: None
runner._restore_secondary_completion_ledgers = lambda *a: None
runner._adapter_credential_claim = lambda *a: None
runner._adapter_listener_claim = lambda *a: None
runner._create_adapter = lambda platform, config: SimpleNamespace(platform=platform)
runner._note_served_profiles([("default", home)])
connected = []
async def connect(adapter, platform):
connected.append(platform)
if platform == Platform.DISCORD:
# Configuration was already read; a second setup operation finishes while
# the first adapter is awaiting its transport handshake.
secrets.write_text(
"DISCORD_BOT_TOKEN=discord-test\nTELEGRAM_BOT_TOKEN=telegram-test\n",
encoding="utf-8",
)
return True
async def after_added(profiles):
pass
runner._connect_initial_adapter_with_timeout = connect
runner._after_profiles_added = after_added
if startup:
await runner._start_secondary_profile_adapters()
else:
await runner.reconcile_served_profiles()
assert connected == [Platform.DISCORD]
await runner.reconcile_served_profiles()
assert connected == [Platform.DISCORD, Platform.TELEGRAM]
await runner.reconcile_served_profiles()
assert connected == [Platform.DISCORD, Platform.TELEGRAM]