fix(cron): multiplexed cron fires and delivers for every served profile — restart-safe scope, guild-scoped shared routes, Desktop ticker allowlist, idle-exit (#107399, #89302, salvage #107413/#104979/#103844/#103742) - #108428
Merged
Conversation
(cherry picked from commit e57f719)
(cherry picked from commit db92ff2)
…ild-scoped routes and profiles without a platforms block
SharedRouteAdapters.get called ProfileRoute.matches without guild_id, so the
documented Discord route shape (guild_id + chat_id) never authorized a cron
target and the satellite fell to standalone delivery ("DISCORD_BOT_TOKEN is
not set" every fire). A cron target has no inbound guild anchor; the route's
own guild_id is passed so its target-exact discriminators decide.
_resolve_target_transport then vetoed the authorized shared transport on the
SATELLITE's platforms.<p>.enabled (absent block or enabled: false), although
that block describes a connector the satellite never runs. The shared hit now
builds the transport directly (keeping the satellite's non-credential platform
settings), and a live native adapter with no config block is no longer read as
"disabled" (#89302) — same normalization the relay path already had.
Fixes #89302
Co-authored-by: web3blind <264741654+web3blind@users.noreply.github.com>
…s down for served satellites The Desktop/serve backend ticked every installed profile (ignoring gateway.multiplex_profile_allowlist) and gated only on the profile's OWN gateway.pid. A satellite served by the default multiplexer has no pid file, so both tickers raced its fires and the Desktop one won nondeterministically — adapter-less standalone delivery, and the environment behind #107485. Homes now come from profiles_to_serve with the default profile's allowlist (the multiplexer's served set); the per-tick gate also consults named_profile_served_by_running_multiplexer. Addresses #107485, #94590 Co-authored-by: fangliquan <fangliquan@qq.com>
…on job runs turn_in_flight read only the dashboard session table; an in-process cron run never registers there, so the watchdog reported "no running turn" and exited mid-job (tool calls then failed with "cannot schedule new futures after interpreter shutdown", the execution was marked unknown, the slot lost). The probe now also consults cron.scheduler.get_running_job_ids — the ledger the gateway shutdown drain already uses. Addresses #107485
…d owns the shared adapters profiles_to_serve(multiplex=True) yields default + allowlist, so a gateway run as `hermes -p <name>` with multiplex on never ticked its own profile's jobs unless allowlisted (which would start a second adapter on the same token). The ticker's home list now unions the active profile. The shared-adapter owner passed to the ticker is the runner's launch profile instead of the literal "default", so that profile's jobs reuse its live adapters rather than the fail-closed empty map. Co-authored-by: Paul Pincente <101599379+pincente@users.noreply.github.com> Co-authored-by: r3x443 <325334945+r3x443@users.noreply.github.com>
…ed route delivery
૮ >ﻌ< ა ci reviewran on 121c624 — docs(multiplex): cron ticker allowlist, named multiplexer, g
|
This was referenced Sep 11, 2026
This was referenced Sep 11, 2026
[Hermes Desktop] multiplex cron ticker recreates archived profiles via profiles_to_serve scan
#94590
Closed
This was referenced Sep 11, 2026
teknium1
added a commit
that referenced
this pull request
Sep 12, 2026
… unified init path The unified runtime copied _init_registries_and_clocks and the cron bootstrap from a pre-fix snapshot: HookRegistry() (one process-wide registry, so served secondaries' hooks/ never load and fire the launch profile's handlers) and default_profile="default" with _multiplex_profile_homes (a --profile <name> multiplexer's own jobs treated as a secondary's). Restore ProfileHookRegistries and _cron_tick_profile_homes / runner._primary_profile_name from main (#108453, #108428).
teknium1
added a commit
that referenced
this pull request
Sep 12, 2026
…ery profile The multiplexing default gateway now serves default + every live named profile under profiles/. profiles_to_serve(multiplex=True) is a pure directory read (tombstoned profiles skipped, never mkdir); every reader — gateway served set, /p/<profile>/ prefixes for api_server + webhook, the named-profile standalone guard, the Desktop cron ticker (its #108428 standdown for a profile owned by a running gateway is unchanged) — drops the allowlist parameter. Config v43 migration deletes the key from user config.yaml; DEFAULT_CONFIG, GatewayConfig and the top-level yaml bridge no longer carry it. BREAKING: anyone who set an allowlist now has their excluded profiles served. Archive or delete a profile you do not want served (Teknium approved).
teknium1
added a commit
that referenced
this pull request
Sep 12, 2026
…ery profile The multiplexing default gateway now serves default + every live named profile under profiles/. profiles_to_serve(multiplex=True) is a pure directory read (tombstoned profiles skipped, never mkdir); every reader — gateway served set, /p/<profile>/ prefixes for api_server + webhook, the named-profile standalone guard, the Desktop cron ticker (its #108428 standdown for a profile owned by a running gateway is unchanged) — drops the allowlist parameter. Config v43 migration deletes the key from user config.yaml; DEFAULT_CONFIG, GatewayConfig and the top-level yaml bridge no longer carry it. BREAKING: anyone who set an allowlist now has their excluded profiles served. Archive or delete a profile you do not want served (Teknium approved).
13 tasks
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Under
gateway.multiplex_profiles, cron jobs now actually fire and deliver for every served profile: the systemd restart-safe handoff no longer dies onUnscopedSecretError, routed satellites deliver through the shared bot for the documentedguild_id + chat_idroute shape (and without aplatforms:block), the Desktop/serve ticker honours the allowlist and yields to the multiplexer, and the SSH-isolated idle-exit no longer kills a running cron job.Changes
cron/scheduler.py::_launch_external_cron_workerbuilds the worker env inside the firing profile's hydrated secret scope, so aterminal.env_passthroughkey present in the gateway process env resolves to the PROFILE's value instead of raising. Salvaged as-is from fix(cron): handle passthrough secrets in multiplexed dispatch #107413 (first commit; the later commits adding a process-env fallback and a per-home passthrough cache were left out — the fallback re-introduces the environ leak the scope exists to prevent).cron/scheduler_preflight.py::SharedRouteAdapters.getpasses the route's ownguild_idtoProfileRoute.matches(a cron target has no inbound guild anchor; the target-exactchat_id/thread_idauthorize).cron/scheduler_delivery.py::_resolve_target_transportbuilds the shared transport from the route-authorized adapter directly, so the satellite's absent orenabled: falseplatforms.<p>block (a connector it never runs) no longer vetoes it.platforms.<p>config block is normalized toPlatformConfig(enabled=True)like the relay path already was; an explicitenabled: falsestill vetoes.hermes_cli/web_server.py::_start_desktop_cron_tickerreads the default profile'smultiplex_profile_allowlist(web_server_cron.py::_default_multiplex_profile_allowlist) and its per-tick gate also consultsnamed_profile_served_by_running_multiplexer, so a satellite the live multiplexer ticks is never raced by the adapter-less Desktop ticker. Single-profile installs get the gate too (salvaged fix(desktop): yield single-profile cron to its running gateway #104979).hermes_cli/web_server_idle_exit.py::turn_in_flightalso consultscron.scheduler.get_running_job_ids()(the ledger the gateway shutdown drain already reads).gateway/run.py::_cron_tick_profile_homesunions the launch profile into the ticker's home list, and the shared-adapter owner passed to the ticker isrunner._primary_profile_name, not the literal"default"(salvaged fix(cron): tick named-profile host store under multiplex #103844 + fix(cron): route a named gateway profile's jobs through the live shared adapters #103742, trimmed to one helper).website/docs/user-guide/multi-profile-gateways.md(allowlist/ticker, named multiplexer, guild-scoped route delivery).Root cause (one line each)
F3: the handoff ran before
_run_one_job_bodyinstalls the fire scope. F5:route.matches(...)was called withoutguild_id, so any route declaring one returned False. F10/#89302:_resolve_target_transportapplied the satellite's native enabled gate to a transport the primary authorized. F6:profiles_to_serve(multiplex=True)without the allowlist + a pid-only gate. F7: the idle probe read only the dashboard session table. Named:profiles_to_serveyields default + allowlist, never the-plaunch profile.Live repro
Script:
/tmp/mux_audit/fix-cron-multiplex/repro.py(tempHERMES_HOME, real imports,profiles/worker,profiles/guest, allowlist[worker], routes{guild G1, chat C1}and{chat C2},env_passthrough: [BW_SESSION]).Before (origin/main
ad03f20dd61):After:
E2E (
e2e_delivery.py, real config loader + DeliveryRouter +_deliver_result, satellite with NOplatforms:block): beforerouted C1 -> "platform 'discord' not configured/enabled"; afterrouted C1 -> error=None, primary sent ['C1'], unroutedC9still fails closed (never the primary bot).Validation
scripts/run_tests.sh tests/cron/scripts/run_tests.sh tests/hermes_cli/test_update_head_moved_gate.py::test_update_success_when_head_moves, fails identically on unmodifiedorigin/main(pre-existing, unrelatedhermes updatetest)scripts/run_tests.sh tests/gateway/origin/main(tests_red_on_base.txt)git diff --checkTests added:
test_guild_scoped_route_authorizes_cron_target_even_when_satellite_has_no_platform_block,test_live_native_adapter_without_platform_block_is_not_treated_as_disabled,test_turn_probe_counts_in_flight_cron_execution,test_desktop_ticker_honours_allowlist_and_yields_to_default_multiplexer,test_cron_shared_adapter_owner_is_the_launch_profile(+ salvagedtest_cron_tick_homes_include_active_named_host,test_single_profile_ticks_only_without_gateway,test_launch_external_worker_uses_restart_safe_scope_and_acknowledgesextension).Credits
e57f719f942, the scope fix) and fix(gateway): stop Desktop cron from contending with profile gateways #102174 (multiplexer stand-down direction; co-authored)Not done / design call
tick()advancesnext_run_atbefore the run). Missed-window catch-up on restart is a lifecycle-contract change — reported, not implemented.tools/env_passthrough.py::_config_passthroughis process-wide).Fixes #107399
Fixes #89302
Addresses #107485
Addresses #94590
Infographic