fix(config): kanban decompose and local-models status keep their fail-open config reads

Repointing the two _load_config copies at load_config_readonly() dropped the
except-Exception guards the old copies had. load_config_readonly runs
ensure_hermes_home(), which can raise FileNotFoundError / HomeInitializationError,
so decompose_task (promises ok=False) and /api/local-models/status (garnish
that must render degraded) would raise / 500 instead. The guard is restored
at both sites with the same breadth the old code had.
This commit is contained in:
teknium1
2026-09-12 23:42:34 -07:00
committed by Teknium
parent 6d05f7238c
commit fd5693bc92
4 changed files with 37 additions and 4 deletions

View File

@@ -195,7 +195,10 @@ class _Routing:
def _load_routing() -> _Routing:
from hermes_cli.config import load_config_readonly
cfg = load_config_readonly()
try:
cfg = load_config_readonly()
except Exception: # decompose_task promises ok=False, never a raise, on config trouble
cfg = {}
kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {}
roster, valid_names = _build_roster()
return _Routing(

View File

@@ -190,8 +190,13 @@ def _router_request(endpoint: Dict[str, Any], path: str, *, timeout: float, payl
return None if payload is not None else json.loads(r.read())
def _load_config() -> dict:
"""Read-only config for status/garnish paths that must render degraded, never 500."""
return _quiet(config_mod.load_config_readonly, {})
def _runtime_section() -> dict:
return (config_mod.load_config_readonly() or {}).get("local_runtime") or {}
return (_load_config() or {}).get("local_runtime") or {}
def _set_runtime_enabled(enabled: bool) -> dict:
@@ -441,7 +446,7 @@ def _active_llamacpp_model_id() -> str | None:
"""The active main model when it is one of ours (config authority: the model.provider + model.default
that /api/model/set writes)."""
def read() -> str | None:
model_section = (config_mod.load_config_readonly() or {}).get("model") or {}
model_section = (_load_config() or {}).get("model") or {}
if str(model_section.get("provider", "")).strip().lower() in _LLAMACPP_PROVIDERS:
return str(model_section.get("default") or model_section.get("name") or "").strip() or None
return None
@@ -631,7 +636,7 @@ def _restart_on_new_tag(job: Dict[str, Any], tag: str, previous: list) -> bool:
return False
_step(job, "restarting", "Switching the running server to the new build")
bootstrap.shutdown_local_runtime()
bootstrap.ensure_local_runtime(config_mod.load_config_readonly(), force=True)
bootstrap.ensure_local_runtime(_load_config(), force=True)
return True

View File

@@ -146,6 +146,19 @@ def test_decompose_fanout_false_invalid_llm_assignee_uses_default(kanban_home):
assert task.assignee == "fallback"
def test_load_routing_falls_back_to_defaults_when_config_unreadable(kanban_home, monkeypatch):
"""decompose_task promises ok=False on expected failures; a config read that raises (missing
profile home, HomeInitializationError) must not escape _load_routing as an exception."""
from hermes_cli import config as config_mod
def _boom():
raise FileNotFoundError("profile home is gone")
monkeypatch.setattr(config_mod, "load_config_readonly", _boom)
routing = decomp._load_routing()
assert routing.default_assignee == "default" and routing.auto_promote is True
def test_decompose_returns_false_when_task_not_triage(kanban_home):
with kbc.connect() as conn:
tid = kb.create_task(conn, title="x") # ready, not triage

View File

@@ -54,6 +54,18 @@ def test_status_shape_and_defaults(client):
assert isinstance(data["models"], list)
def test_status_renders_degraded_when_config_cannot_be_read(client, monkeypatch):
"""The status pane is garnish: an unreadable/uninitialized config renders defaults, never a 500."""
from hermes_cli import config as config_mod
def _boom():
raise FileNotFoundError("profile home is gone")
monkeypatch.setattr(config_mod, "load_config_readonly", _boom)
r = client.get("/api/local-models/status")
assert r.status_code == 200 and r.json()["enabled"] is False
def test_status_lists_staged_models_with_labels(client, tmp_path):
from hermes_cli.local_runtime.bootstrap import models_dir