cron namespace collision causing gateway startup crashes
NousResearch/hermes-agent · cluster 49410
Data as of
Nothing open on GitHub. Fix merged in #49913. Check it covers your case.
The short answer
- Known?
- Yes. 10 related items grouped here, 4 issues already closed.
- Fix event
- A fix was merged to main in PR #49913. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PRs #49913, #51702. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A package naming conflict between plugins/cron and the core cron module caused ModuleNotFoundError on gateway startup in v0.17.0. Multiple fixes address the collision via renaming, sys.path corrections, and import adjustments.
Open issues0
Open PRs0
Fixes on record
-
#49913 fix(gateway): correct sys.path insertion in all migrated platform adapters (cron namespace collision)
files:
plugins/platforms/discord/adapter.py,plugins/platforms/raft/adapter.py,plugins/platforms/slack/adapter.py,plugins/platforms/telegram/adapter.py,plugins/platforms/whatsapp/adapter.pyShow the diff
-
plugins/platforms/discord/adapter.py@@ -98,7 +98,7 @@ def __init__(self, id: int) -> None: # noqa: A002 - matches discord API import sys from pathlib import Path as _Path -sys.path.insert(0, str(_Path(__file__).resolve().parents[2])) +sys.path.insert(0, str(_Path(__file__).resolve().parents[3])) from gateway.config import Platform, PlatformConfig -
plugins/platforms/raft/adapter.py@@ -36,7 +36,7 @@ import sys from pathlib import Path as _Path -sys.path.insert(0, str(_Path(__file__).resolve().parents[2])) +sys.path.insert(0, str(_Path(__file__).resolve().parents[3])) from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( -
plugins/platforms/slack/adapter.py@@ -34,7 +34,7 @@ import sys from pathlib import Path as _Path -sys.path.insert(0, str(_Path(__file__).resolve().parents[2])) +sys.path.insert(0, str(_Path(__file__).resolve().parents[3])) from gateway.config import Platform, PlatformConfig from gateway.platforms.helpers import MessageDeduplicator -
plugins/platforms/telegram/adapter.py@@ -63,7 +63,7 @@ class _MockContextTypes: import sys from pathlib import Path as _Path -sys.path.insert(0, str(_Path(__file__).resolve().parents[2])) +sys.path.insert(0, str(_Path(__file__).resolve().parents[3])) from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( -
plugins/platforms/whatsapp/adapter.py@@ -178,7 +178,7 @@ def _terminate_bridge_process(proc, *, force: bool = False) -> None: return import sys -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +sys.path.insert(0, str(Path(__file__).resolve().parents[3])) from gateway.config import Platform, PlatformConfig from gateway.platforms.whatsapp_common import WhatsAppBehaviorMixin
-
-
#51702 fix(cron): rename plugins/cron → plugins/cron_providers to kill the core-cron shadowing class (#50872)
files:
cron/scheduler_provider.py,gateway/platforms/api_server.py,hermes_cli/config.py,hermes_cli/web_server.py,plugins/cron_providers/__init__.py,plugins/cron_providers/chronos/__init__.py,plugins/cron_providers/chronos/_nas_client.py,plugins/cron_providers/chronos/plugin.yaml,plugins/cron_providers/chronos/verify.py,scripts/release.py,tests/cron/test_scheduler_provider.py,tests/gateway/test_cron_fire_webhook.py,tests/hermes_cli/test_cron_fire_dashboard.py,tests/plugins/test_chronos_cron.py,tests/plugins/test_chronos_verify.pyShow the diff
-
cron/scheduler_provider.py@@ -14,7 +14,7 @@ The built-in InProcessCronScheduler runs the historical 60s daemon-thread ticker. Alternative providers (e.g. Chronos, a NAS-mediated managed-cron -provider for scale-to-zero deployments) live under plugins/cron/<name>/ and are +provider for scale-to-zero deployments) live under plugins/cron_providers/<name>/ and are selected via the `cron.provider` config key (empty = built-in). """ from __future__ import annotations @@ -134,7 +134,7 @@ def resolve_cron_scheduler() -> "CronScheduler": return InProcessCronScheduler() try: - from plugins.cron import load_cron_scheduler + from plugins.cron_providers import load_cron_scheduler provider = load_cron_scheduler(name) if provider is None: logger.warning("cron.provider '%s' not found; using built-in ticker", name) -
gateway/platforms/api_server.py@@ -3448,7 +3448,7 @@ async def _handle_cron_fire(self, request: "web.Request") -> "web.Response": against double-fire on a NAS/scheduler retry. """ from hermes_cli.config import cfg_get, load_config - from plugins.cron.chronos.verify import get_fire_verifier + from plugins.cron_providers.chronos.verify import get_fire_verifier auth = request.headers.get("Authorization", "") token = [redacted:secret-assignment] if auth.startswith("Bearer ") else "" -
hermes_cli/config.py@@ -2340,7 +2340,7 @@ def _ensure_hermes_home_managed(home: Path): "cron": { # Active cron SCHEDULER provider (Axis B — the trigger that decides # WHEN a due job fires). Empty string = the built-in in-process 60s - # ticker (default). Name an installed provider (plugins/cron/<name>/ or + # ticker (default). Name an installed provider (plugins/cron_providers/<name>/ or # $HERMES_HOME/plugins/<name>/) to relocate the trigger — e.g. "chronos", # the NAS-mediated managed-cron provider for scale-to-zero deployments. # An unknown or unavailable provider falls back to the built-in, so cron -
hermes_cli/web_server.py@@ -7916,7 +7916,7 @@ async def cron_fire_webhook(request: Request): Returns 202 immediately and runs the job in the background so a long agent turn never trips NAS's HTTP timeout. """ - from plugins.cron.chronos.verify import get_fire_verifier + from plugins.cron_providers.chronos.verify import get_fire_verifier auth = request.headers.get("Authorization", "") token = [redacted:secret-assignment] if auth.startswith("Bearer ") else "" -
plugins/cron_providers/__init__.py@@ -2,7 +2,7 @@ Scans two directories for cron scheduler provider plugins: -1. Bundled providers: ``plugins/cron/<name>/`` (shipped with hermes-agent) +1. Bundled providers: ``plugins/cron_providers/<name>/`` (shipped with hermes-agent) 2. User-installed providers: ``$HERMES_HOME/plugins/<name>/`` Each subdirectory must contain ``__init__.py`` with a class implementing the @@ -19,7 +19,7 @@ config.yaml (empty = built-in). See ``cron.scheduler_provider.resolve_cron_scheduler``. Usage: - from plugins.cron import discover_cron_schedulers, load_cron_scheduler + from plugins.cron_providers import discover_cron_schedulers, load_cron_scheduler available = discover_cron_schedulers() # [(name, desc, available), ...] provider = load_cron_scheduler("chronos") # CronScheduler instance @@ -52,7 +52,7 @@ def _register_synthetic_package(name: str, search_locations: List[str]) -> None: in ``sys.modules``, any relative import inside the plugin (``from . import config``) fails with ``ModuleNotFoundError: No module named '_hermes_user_cron'`` — the same - reason the loader already registers ``plugins`` and ``plugins.cron`` for + reason the loader already registers ``plugins`` and ``plugins.cron_providers`` for bundled providers. """ if name in sys.modules: @@ -101,7 +101,7 @@ def _iter_provider_dirs() -> List[Tuple[str, Path]]: seen: set = set() dirs: List[Tuple[str, Path]] = [] - # 1. Bundled providers (plugins/cron/<name>/) + # 1. Bundled providers (plugins/cron_providers/<name>/) if _CRON_PLUGINS_DIR.is_dir(): for child in sorted(_CRON_PLUGINS_DIR.iterdir()): if not child.is_dir() or child.name.startswith(("_", ".")): @@ -190,7 +190,7 @@ def discover_cron_schedulers() -> List[Tuple[str, str, bool]]: def load_cron_scheduler(name: str) -> Optional["CronScheduler"]: # noqa: F821 """Load and return a CronScheduler instance by name. - Checks both bundled (``plugins/cron/<name>/``) and user-installed + Checks both bundled (``plugins/cron_providers/<name>/``) and user-installed (``$HERMES_HOME/plugins/<name>/``) directories. Bundled takes precedence on name collisions. @@ -223,7 +223,7 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: # # Use a separate namespace for user-installed plugins so they don't # collide with bundled providers in sys.modules. _is_bundled = _CRON_PLUGINS_DIR in provider_dir.parents or provider_dir.parent == _CRON_PLUGINS_DIR - module_name = f"plugins.cron.{name}" if _is_bundled else f"{_USER_NAMESPACE}.{name}" + module_name = f"plugins.cron_providers.{name}" if _is_bundled else f"{_USER_NAMESPACE}.{name}" init_file = provider_dir / "__init__.py" if not init_file.exists(): @@ -236,7 +236,7 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: # mod = cached else: # Ensure the parent packages are registered (for relative imports) - for parent in ("plugins", "plugins.cron"): + for parent in ("plugins", "plugins.cron_providers"): if parent not in sys.modules: parent_path = Path(__file__).parent if parent == "plugins": @@ -270,6 +270,7 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: # mod = importlib.util.module_from_spec(spec) sys.modules[module_name] = mod + loaded_submodules = [] # Register submodules so relative imports work # e.g., "from ._nas_client import NasCronClient" in the chronos plugin @@ -287,6 +288,7 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: # sys.modules[full_sub_name] = sub_mod try: sub_spec.loader.exec_module(sub_mod) + loaded_submodules.append((sub_name, sub_mod)) except Exception as e: logger.debug("Failed to load submodule %s: %s", full_sub_name, e) @@ -297,6 +299,16 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: # sys.modules.pop(module_name, None) return None + # Manual importlib loading bypasses the normal import machinery that + # binds child modules onto their parent packages. Restore that shape so + # later dotted imports and pytest monkeypatch paths resolve normally. + parent_name, child_name = module_name.rsplit(".", 1) + parent_mod = sys.modules.get(parent_name) + if parent_mod is not None: + setattr(parent_mod, child_name, mod) + for sub_name, sub_mod in loaded_submodules: + setattr(mod, sub_name, sub_mod) + # Try register(ctx) pattern first (how our plugins are written) if hasattr(mod, "register"): collector = _ProviderCollector() -
plugins/cron_providers/chronos/__init__.py@@ -235,7 +235,7 @@ def fire_due(self, job_id: str, *, adapters: Any = None, loop: Any = None) -> bo def register(ctx) -> None: """Plugin entrypoint — register the Chronos provider with the loader. - Mirrors the memory-plugin shape; plugins/cron discovery calls this and + Mirrors the memory-plugin shape; plugins/cron_providers discovery calls this and collects the provider via register_cron_scheduler. """ ctx.register_cron_scheduler(ChronosCronScheduler()) -
scripts/release.py@@ -46,6 +46,7 @@ # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { "[redacted:email]": "wgu9", # PR #51468 salvage (WSL/no-systemd orphan gateway tracking, #51325) + "[redacted:email]": "uperLu", # PR #50958 salvage (rename plugins/cron → plugins/cron_providers; #50872) "[redacted:email]": "s010mn", # PR #29221 salvage (ollama-cloud reasoning_effort xhigh→max) "[redacted:email]": "jeeves-assistant", # PR #50771 (computer-use CuaDriver vision capture routing) "[redacted:email]": "ScotterMonk", # PR #50145 salvage (cron output truncation: adapter-aware chunking, #50126) -
tests/cron/test_scheduler_provider.py@@ -172,20 +172,42 @@ def test_default_config_cron_provider_is_empty(): def test_discover_cron_schedulers_returns_list(): - """Discovery returns a list. May be empty — the built-in is core, not - discovered, and no bundled non-default provider ships yet.""" - from plugins.cron import discover_cron_schedulers + """Discovery returns bundled non-default providers. + + The built-in is core, not discovered here. + """ + from plugins.cron_providers import discover_cron_schedulers result = discover_cron_schedulers() assert isinstance(result, list) + assert any(name == "chronos" for name, _desc, _available in result) def test_load_unknown_cron_scheduler_returns_none(): - from plugins.cron import load_cron_scheduler + from plugins.cron_providers import load_cron_scheduler assert load_cron_scheduler("does-not-exist-xyz") is None +def test_cron_provider_package_does_not_shadow_core_cron_package(monkeypatch): + """Putting plugins/ first on sys.path must not hide the core cron package.""" + from importlib.machinery import PathFinder + from pathlib import Path + + repo_root = Path(__file__).resolve().parents[2] + + monkeypatch.syspath_prepend(str(repo_root)) + monkeypatch.syspath_prepend(str(repo_root / "plugins")) + + cron_spec = PathFinder.find_spec("cron") + assert cron_spec is not None + assert Path(cron_spec.origin).resolve() == repo_root / "cron" / "__init__.py" + + jobs_spec = PathFinder.find_spec("cron.jobs", [str(repo_root / "cron")]) + assert jobs_spec is not None + assert Path(jobs_spec.origin).resolve() == repo_root / "cron" / "jobs.py" + + def test_resolve_defaults_to_builtin(monkeypatch): """Empty cron.provider → built-in.""" import hermes_cli.config as cfg @@ -219,7 +241,7 @@ def test_resolve_unknown_provider_falls_back_to_builtin(monkeypatch): def test_resolve_unavailable_provider_falls_back(monkeypatch): """A provider that loads but reports is_available()==False → built-in.""" import hermes_cli.config as cfg - import plugins.cron as pc + import plugins.cron_providers as pc from cron import scheduler_provider as sp from cron.scheduler_provider import CronScheduler @@ -243,7 +265,7 @@ def start(self, stop_event, **kw): def test_resolve_available_provider_is_used(monkeypatch): """A provider that loads and is available is returned (not the fallback).""" import hermes_cli.config as cfg - import plugins.cron as pc + import plugins.cron_providers as pc from cron import scheduler_provider as sp from cron.scheduler_provider import CronScheduler -
tests/gateway/test_cron_fire_webhook.py@@ -53,7 +53,7 @@ async def test_valid_token_accepts_and_fires(adapter, monkeypatch): monkeypatch.setattr("cron.scheduler_provider.resolve_cron_scheduler", lambda: spy) # verifier returns claims (valid token) monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire", "aud": "agent:x"}), ) @@ -80,7 +80,7 @@ async def test_invalid_token_401_and_no_fire(adapter, monkeypatch): spy = _SpyProvider() monkeypatch.setattr("cron.scheduler_provider.resolve_cron_scheduler", lambda: spy) monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: None), # verification fails ) @@ -114,7 +114,7 @@ async def test_missing_job_id_400(adapter, monkeypatch): spy = _SpyProvider() monkeypatch.setattr("cron.scheduler_provider.resolve_cron_scheduler", lambda: spy) monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire"}), ) @@ -134,7 +134,7 @@ async def test_fire_does_not_require_api_server_key(adapter, monkeypatch): spy = _SpyProvider() monkeypatch.setattr("cron.scheduler_provider.resolve_cron_scheduler", lambda: spy) monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire"}), ) -
tests/hermes_cli/test_cron_fire_dashboard.py@@ -61,7 +61,7 @@ def test_bad_token_401(monkeypatch): gate). fire_due must NOT run.""" fired = [] monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: None), # verification fails ) monkeypatch.setattr(web_server, "_find_cron_job_profile", lambda jid: "default") @@ -82,7 +82,7 @@ def test_bad_token_401(monkeypatch): def test_missing_job_id_400(monkeypatch): monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire"}), ) client, pa, ph = _client(auth_required=False) @@ -100,7 +100,7 @@ def test_unknown_job_200_gone(monkeypatch): """Valid token but the job isn't found in any profile -> 200 'gone' (NAS shouldn't retry a fire for a cancelled/completed job).""" monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire"}), ) monkeypatch.setattr(web_server, "_find_cron_job_profile", lambda jid: None) @@ -121,7 +121,7 @@ def test_valid_token_accepts_and_fires(monkeypatch): profile.""" fired = [] monkeypatch.setattr( - "plugins.cron.chronos.verify.get_fire_verifier", + "plugins.cron_providers.chronos.verify.get_fire_verifier", lambda: (lambda **kw: {"purpose": "cron_fire", "aud": "agent:x"}), ) monkeypatch.setattr(web_server, "_find_cron_job_profile", lambda jid: "default") -
tests/plugins/test_chronos_cron.py@@ -21,7 +21,7 @@ def temp_home(tmp_path, monkeypatch): @pytest.fixture def chronos(monkeypatch): """A ChronosCronScheduler with a fake NAS client capturing calls.""" - from plugins.cron.chronos import ChronosCronScheduler + from plugins.cron_providers.chronos import ChronosCronScheduler class FakeClient: def __init__(self): @@ -47,7 +47,7 @@ def list_armed(self): fake = FakeClient() prov._client = fake # callback_url is read via _cfg; patch the module helper to avoid config. - monkeypatch.setattr("plugins.cron.chronos._cfg", + monkeypatch.setattr("plugins.cron_providers.chronos._cfg", lambda *k, default="": "https://agent.example/" if k[-1] == "callback_url" else "https://portal.test") return prov, fake @@ -55,15 +55,15 @@ def list_armed(self): # -- is_available ------------------------------------------------------------- def test_is_available_false_without_config(temp_home, monkeypatch): - from plugins.cron.chronos import ChronosCronScheduler + from plugins.cron_providers.chronos import ChronosCronScheduler - monkeypatch.setattr("plugins.cron.chronos._cfg", lambda *k, default="": "") + monkeypatch.setattr("plugins.cron_providers.chronos._cfg", lambda *k, default="": "") assert ChronosCronScheduler().is_available() is False def test_is_available_true_with_config_and_token(temp_home, monkeypatch): - import plugins.cron.chronos as mod - from plugins.cron.chronos import ChronosCronScheduler + import plugins.cron_providers.chronos as mod + from plugins.cron_providers.chronos import ChronosCronScheduler monkeypatch.setattr(mod, "_cfg", lambda *k, default="": "https://x" ) monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", @@ -73,8 +73,8 @@ def test_is_available_true_with_config_and_token(temp_home, monkeypatch): def test_is_available_makes_no_network(temp_home, monkeypatch): """is_available must not construct the NAS client / hit network.""" - import plugins.cron.chronos as mod - from plugins.cron.chronos import ChronosCronScheduler + import plugins.cron_providers.chronos as mod + from plugins.cron_providers.chronos import ChronosCronScheduler monkeypatch.setattr(mod, "_cfg", lambda *k, default="": "https://x") monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", -
tests/plugins/test_chronos_verify.py@@ -54,7 +54,7 @@ def _base_claims(**over): def test_valid_token_returns_claims(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys token = [redacted:secret-assignment] _base_claims()) @@ -66,7 +66,7 @@ def test_valid_token_returns_claims(rsa_keys): def test_wrong_audience_rejected(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys token = [redacted:secret-assignment] _base_claims(aud="agent:someone-else")) @@ -76,7 +76,7 @@ def test_wrong_audience_rejected(rsa_keys): def test_missing_purpose_rejected(rsa_keys): """A general agent JWT (no purpose=cron_fire) can't fire jobs.""" - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys claims = _base_claims() @@ -87,7 +87,7 @@ def test_missing_purpose_rejected(rsa_keys): def test_wrong_purpose_rejected(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys token = [redacted:secret-assignment] _base_claims(purpose="inference")) @@ -96,7 +96,7 @@ def test_wrong_purpose_rejected(rsa_keys): def test_expired_token_rejected(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys now = int(time.time()) @@ -106,7 +106,7 @@ def test_expired_token_rejected(rsa_keys): def test_wrong_issuer_rejected(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys token = [redacted:secret-assignment] _base_claims(iss="https://evil.example")) @@ -118,7 +118,7 @@ def test_tampered_signature_rejected(rsa_keys): """A token signed by a DIFFERENT key must fail signature verification.""" from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token _, pub = rsa_keys attacker = rsa.generate_private_key(public_exponent=65537, key_size=2048) @@ -135,7 +135,7 @@ def test_tampered_signature_rejected(rsa_keys): def test_no_key_configured_refuses(rsa_keys): """No JWKS/key configured → refuse (never fall back to unsigned decode).""" - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, _ = rsa_keys token = [redacted:secret-assignment] _base_claims()) @@ -144,7 +144,7 @@ def test_no_key_configured_refuses(rsa_keys): def test_empty_token_refused(rsa_keys): - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token _, pub = rsa_keys assert verify_nas_fire_token(token="", expected_audience=AUD, jwks_or_key=pub) is None @@ -152,7 +152,7 @@ def test_empty_token_refused(rsa_keys): def test_jwks_url_path_resolves_key(rsa_keys, monkeypatch): """The JWKS-URL branch resolves the signing key via PyJWKClient.""" - from plugins.cron.chronos.verify import verify_nas_fire_token + from plugins.cron_providers.chronos.verify import verify_nas_fire_token priv, pub = rsa_keys token = [redacted:secret-assignment] _base_claims()) @@ -177,6 +177,6 @@ def get_signing_key_from_jwt(self, tok): def test_get_fire_verifier_returns_nas_verifier(): - from plugins.cron.chronos.verify import get_fire_verifier, verify_nas_fire_token + from plugins.cron_providers.chronos.verify import get_fire_verifier, verify_nas_fire_token assert get_fire_verifier() is verify_nas_fire_token
-
4 PRs closed without merging
From the close comments: merged via another change 3 · duplicate 1
-
#49424 fix(gateway): move cron.scheduler_provider import to module level · duplicate →
#49414,#49410 -
#49431 fix(gateway): correct sys.path insertion in plugins to prevent cron namespace collision (#49410) · merged via another change →
4c206b9,#49913 -
#50958 fix(cron): avoid provider package shadowing core cron · merged via another change →
#51702 -
#51162 fix(cron): rename plugins/cron to plugins/cron_providers to resolve WebUI import collision · merged via another change →
#50958,#51702
Closed issues · 4
- Closed#49410[Bug]: Gateway crashes on startup with ModuleNotFoundError: No module named 'cron.scheduler_provider'
- Closed#49824[Bug] v0.17.0 gateway fails to start: ModuleNotFoundError cron.scheduler_provider
- Closed#50872plugins/cron/ package name collision with cron/ — ModuleNotFoundError in WebUI
- Closed#51405[Bug] Missing cron.scheduler_provider in 0.17.0 - Gateway crashes on startup with Feishu
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.24: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab