diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index 8ca1e8d1d4..e460fd75c2 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -366,16 +366,21 @@ class HonchoClientConfig: or raw.get("recallMode") or "hybrid" ), + # Migration guard: existing configs without an explicit + # observationMode keep the old "unified" default so users + # aren't silently switched to full bidirectional observation. + # New installations (no host block, no credentials) get + # "directional" (all observations on) as the new default. observation_mode=_normalize_observation_mode( host_block.get("observationMode") or raw.get("observationMode") - or "directional" + or ("unified" if _explicitly_configured else "directional") ), **_resolve_observation( _normalize_observation_mode( host_block.get("observationMode") or raw.get("observationMode") - or "directional" + or ("unified" if _explicitly_configured else "directional") ), host_block.get("observation") or raw.get("observation"), ), diff --git a/tests/honcho_plugin/test_client.py b/tests/honcho_plugin/test_client.py index 6a49ce514c..71f48351ee 100644 --- a/tests/honcho_plugin/test_client.py +++ b/tests/honcho_plugin/test_client.py @@ -437,6 +437,69 @@ class TestProfileScopedConfig: assert config.peer_name == "dreamer-user" +class TestObservationModeMigration: + """Existing configs without explicit observationMode keep 'unified' default.""" + + def test_existing_config_defaults_to_unified(self, tmp_path): + """Config with host block but no observationMode → 'unified' (old default).""" + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "hosts": {"hermes": {"enabled": True, "aiPeer": "hermes"}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.observation_mode == "unified" + + def test_new_config_defaults_to_directional(self, tmp_path): + """Config with no host block and no credentials → 'directional' (new default).""" + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({})) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.observation_mode == "directional" + + def test_explicit_directional_respected(self, tmp_path): + """Existing config with explicit observationMode → uses what's set.""" + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "hosts": {"hermes": {"enabled": True, "observationMode": "directional"}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.observation_mode == "directional" + + def test_explicit_unified_respected(self, tmp_path): + """Existing config with explicit observationMode unified → stays unified.""" + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "observationMode": "unified", + "hosts": {"hermes": {"enabled": True}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + assert cfg.observation_mode == "unified" + + def test_granular_observation_overrides_preset(self, tmp_path): + """Explicit observation object overrides both preset and migration default.""" + cfg_file = tmp_path / "config.json" + cfg_file.write_text(json.dumps({ + "apiKey": "k", + "hosts": {"hermes": { + "enabled": True, + "observation": { + "user": {"observeMe": True, "observeOthers": False}, + "ai": {"observeMe": False, "observeOthers": True}, + }, + }}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=cfg_file) + # observation_mode falls back to "unified" (migration), but + # granular booleans from the observation object win + assert cfg.user_observe_me is True + assert cfg.user_observe_others is False + assert cfg.ai_observe_me is False + assert cfg.ai_observe_others is True + + class TestResetHonchoClient: def test_reset_clears_singleton(self): import plugins.memory.honcho.client as mod