diff --git a/navi/overlay/apply.py b/navi/overlay/apply.py index 65792ac..f4a93a3 100644 --- a/navi/overlay/apply.py +++ b/navi/overlay/apply.py @@ -189,6 +189,21 @@ # ── live refresh ──────────────────────────────────────────────────────────── +def _staged_sections(previous: dict, merged: dict) -> list[str]: + """Sections that changed but only take effect at the next start. + + ``llm`` and ``mcp_servers`` are consumed while the container is built — + backends and the MCP pool do not survive a swap — so a refresh that + changes them must say "restart", not quietly report the document applied. + ``profiles`` are rebuilt live and settings are diffed separately. + """ + staged = [] + for section in ("llm", "mcp_servers"): + if (previous.get(section) or {}) != (merged.get(section) or {}): + staged.append(section) + return staged + + def _ack(base_settings, fetched: Fetched, status: str, restart_required: list[str], errors: list[str]) -> None: if not base_settings.hive_url: @@ -276,6 +291,7 @@ # were handed back to local values, so it must run before we replace # ``state.merged``. hot, restart = apply_settings_live(validation.merged.get("settings") or {}) + restart += _staged_sections(state.merged or {}, validation.merged) state.merged = validation.merged apply_profiles_registry_from_state() diff --git a/tests/unit/overlay/test_apply.py b/tests/unit/overlay/test_apply.py index 9c4094d..deb2727 100644 --- a/tests/unit/overlay/test_apply.py +++ b/tests/unit/overlay/test_apply.py @@ -252,6 +252,40 @@ assert state.last_error is None +def test_an_llm_change_is_staged_not_claimed_applied(state, monkeypatch): + monkeypatch.setattr( + apply_mod, + "fetch_layers", + lambda b: Fetched( + version=2, + layers=[{"name": "g", "config": {"llm": {"probe": {"interval_sec": 30}}}}], + hash="sha256:llm", + ), + ) + result = refresh("test") + assert result["status"] == "staged" + assert "llm" in result["restart_required"] + # The document is remembered, so a later start picks it up from the cache. + assert state.merged["llm"]["probe"]["interval_sec"] == 30 + + +def test_a_settings_only_refresh_does_not_stage_llm(state, monkeypatch): + monkeypatch.setattr( + apply_mod, + "fetch_layers", + lambda b: Fetched( + version=3, + layers=[ + {"name": "g", "config": {"settings": {"llm_complete_timeout": 42}}} + ], + hash="sha256:s", + ), + ) + result = refresh("test") + assert "llm" not in result["restart_required"] + assert "mcp_servers" not in result["restart_required"] + + def test_refresh_staged_when_a_restart_is_needed(state, monkeypatch): monkeypatch.setattr( apply_mod,