diff --git a/app/ui/features/generate/inputs.js b/app/ui/features/generate/inputs.js index 11991f72..852825c7 100644 --- a/app/ui/features/generate/inputs.js +++ b/app/ui/features/generate/inputs.js @@ -41,7 +41,7 @@ export async function buildInputs({ project, scene, slots, expand, frames, hooks if (!(body && typeof body.text === "string")) failedExpand = true; } // Features that add inputs of their own (a Chat comment for the enhancer, say) say so through a hook: { inputs: {slot: {name: value}}, notes: [text] }. - const notes = failedExpand ? ["The prompt could not be expanded ($variables, anchor, postfix): the text was sent as typed."] : []; + const notes = failedExpand ? ["The prompt could not be expanded (shortcuts, $variables, anchor, postfix): the text was sent as typed."] : []; for (const hook of hooks) { let out; try { out = (await hook({ project, scene, slots })) || {}; } catch (err) { notes.push(`An add-on's input could not be added to this run: ${err.message}`); continue; } diff --git a/core/control.py b/core/control.py index 610d13c4..08971533 100644 --- a/core/control.py +++ b/core/control.py @@ -204,7 +204,8 @@ def partition(specs: Iterable, settings) -> Tuple[List, List[str]]: """(kept, notes): `specs` without the ones the project turned off or that are quarantined.""" specs = list(specs) off, everything = off_by_project(settings), all_off(settings) - known = {s.id for s in specs} + from . import registry + known = {s.id for s in specs} | set(registry.current().specs) # the list passed in is filtered (traits, capability); "loaded" is the registry for gone in sorted(off - known): # a saved "turn this module off" for a module that is no longer loaded log.once(f"stale_off:{gone}", log.WARNING, "FunPack modules", f"{gone!r} is turned off in this project's settings, but no module with that id is loaded: ignored") diff --git a/core/probe.py b/core/probe.py index 52599ecc..2e9a12c0 100644 --- a/core/probe.py +++ b/core/probe.py @@ -111,7 +111,7 @@ def detect(path: Path, registry=None) -> dict: matched = claims(keyset) except Exception as exc: # noqa: BLE001 from . import log - log.failed(f"{spec.id}.detect", exc) + log.once(f"provider:{spec.id}.detect:{type(exc).__name__}", log.WARNING, f"{spec.id}.detect", f"failed while answering -- {type(exc).__name__}: {exc}") continue if matched: traits = None # unknown, not empty: a module that never says hides nothing @@ -121,7 +121,7 @@ def detect(path: Path, registry=None) -> dict: traits = list(probe_traits(keyset)) except Exception as exc: # noqa: BLE001 from . import log - log.failed(f"{spec.id}.probe_traits", exc) + log.once(f"provider:{spec.id}.probe_traits:{type(exc).__name__}", log.WARNING, f"{spec.id}.probe_traits", f"failed while answering -- {type(exc).__name__}: {exc}") return {"module": spec.id, "title": spec.title, "detected": True, "reason": f"{p.name}: {spec.title}", "traits": traits} return {"module": None, "title": None, "detected": False, diff --git a/core/routes.py b/core/routes.py index c4784b99..007cb792 100644 --- a/core/routes.py +++ b/core/routes.py @@ -116,9 +116,10 @@ def modules(rescan=False): return registry_mod.current(rescan) -def _provider_failed(what, exc): +def _provider_failed(what, exc, spec=None): """A provider raised while answering: said once per provider and kind of error, then skipped.""" - log.once(f"provider:{what}:{type(exc).__name__}", log.WARNING, what, f"failed while answering -- {type(exc).__name__}: {exc}") + who = f"{what} ({spec.id})" if spec is not None else what + log.once(f"provider:{who}:{type(exc).__name__}", log.WARNING, who, f"failed while answering -- {type(exc).__name__}: {exc}") def settings_sinks(): @@ -128,7 +129,7 @@ def settings_sinks(): try: sink = make() except Exception as exc: # noqa: BLE001 - _provider_failed("settings_sink", exc) + _provider_failed("settings_sink", exc, _spec) continue if isinstance(sink, dict) and sink.get("node") and sink.get("input"): found.append({"node": sink["node"], "input": sink["input"]}) @@ -268,7 +269,7 @@ def register(routes, prefix=None): try: offered = make() except Exception as exc: # noqa: BLE001 - _provider_failed("pipeline_presets", exc) + _provider_failed("pipeline_presets", exc, spec) continue for preset in (offered or []): if isinstance(preset, dict) and preset.get("id") and preset.get("slots"): @@ -306,7 +307,7 @@ def register(routes, prefix=None): try: presets += list(make() or []) except Exception as exc: # noqa: BLE001 - _provider_failed("pipeline_presets", exc) + _provider_failed("pipeline_presets", exc, _spec) reg = modules() rows = readiness.machine(sysinfo.collect().get("disk", {}).get("free_gb")) rows += readiness.models(counts) diff --git a/core/tests/test_control.py b/core/tests/test_control.py index 7e83b070..78804d19 100644 --- a/core/tests/test_control.py +++ b/core/tests/test_control.py @@ -139,3 +139,15 @@ def test_a_saved_turn_off_for_a_module_that_is_gone_is_said_once(): control.partition([spec("b")], {"_off": {"modules": ["gone"]}}) said = [r for r in log.history() if "'gone'" in r["message"]] assert len(said) == 1 and "ignored" in said[0]["message"] + + +def test_a_turned_off_module_that_is_loaded_but_not_in_the_filtered_list_is_not_called_gone(monkeypatch): + """Callers pass a filtered list (by capability, by the model's traits); "gone" means absent from the registry.""" + from core import log, registry + log._reset() + reg = registry.Registry() + reg.specs["sharpen"] = spec("sharpen") + monkeypatch.setattr(registry, "current", lambda rescan=False: reg) + control.partition([spec("other")], {"_off": {"modules": ["sharpen"]}}) + control.partition([spec("other")], {"_off": {"all": True}}) + assert not [r for r in log.history() if "no module with that id" in r["message"]] diff --git a/core/tests/test_registry.py b/core/tests/test_registry.py index e8bcbedd..e2aab3a1 100644 --- a/core/tests/test_registry.py +++ b/core/tests/test_registry.py @@ -300,3 +300,14 @@ def test_a_duplicate_id_is_said_in_the_log_not_only_in_settings(): reg.add(validate({"id": "dup", "title": "A", "mount": "m"}, source="one")) reg.add(validate({"id": "dup", "title": "B", "mount": "m"}, source="two")) assert any("duplicate id" in r["message"] for r in log.history()) + + +def test_route_providers_that_raise_are_said_by_name_once_each(): + from core import log, routes + log._reset() + a = type("S", (), {"id": "mod_a"})() + b = type("S", (), {"id": "mod_b"})() + for s in (a, b, a, b): + routes._provider_failed("settings_sink", KeyError("x"), s) + said = [r for r in log.history() if "failed while answering" in r["message"]] + assert sorted(r["source"] for r in said) == ["settings_sink (mod_a)", "settings_sink (mod_b)"]