Skip to content

Cron jobs run on systemd gateways without a user D-Bus session (salvage #102431) - #109311

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/cron-scope-degrade-salvage-102431
Sep 12, 2026
Merged

kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:fix/cron-scope-degrade-salvage-102431

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented Sep 12, 2026 •

Copy link
Copy Markdown

Cron jobs on a systemd-managed gateway with no user D-Bus session (containers, minimal LXCs, a service user without linger) run again instead of failing at dispatch on every fire.

Salvage of #102431 by @kiwipaulrob — the fix commit is cherry-picked with authorship preserved; the second commit folds the review findings on top. Closes #102431's symptom; residual population after #105346 is hosts with genuinely no user manager.

Behaviour

  • tools/process_registry.py::restart_safe_gateway_child_argv returns a GatewayChildDispatch(mode, argv) with mode ∈ {in_process, scoped, degraded}, so a degraded dispatch can never collapse into the "not managed, stay in-process" sentinel (the v1 blocker).
  • Cron (cron/scheduler.py::_launch_external_cron_worker): scoped and degraded both launch an external worker with the existing fix(cron): preserve active turns across gateway restarts (salvage #101877) #101940 ownership handoff; degraded runs the direct command (process separation kept, cgroup isolation lost) and warns once per gateway process. New config key cron.require_restart_safe_scope (default false) restores the fail-closed raise with the enable-linger remedy.
  • Kanban (hermes_cli/kanban_db_dispatch.py) passes require_restart_safe_scope=True — long-lived workers stay fail-closed; its no-run-id guard fails closed for any non-in_process mode.
  • Documented in website/docs/user-guide/features/cron.md ("Restart-safe workers under systemd").

Follow-up commit (review findings)

  • The two real-helper scheduler tests were unmarked and red on macOS/Windows; the survivor is linux_only.
  • Bare os.getuid() in the warning tripped check-windows-footguns.py --all (lint lane); the remedy text is built once and passed into the warning, which also fixes the "scope binary vanished" case warning about a missing D-Bus.
  • Tests trimmed to the invariants: degraded ≠ in_process and warns once (helper); default config still Popens externally (scheduler, real helper + real config plumbing). The Popen/ack scaffold is one shared helper instead of three copies.
  • GatewayChildDispatch.reason removed (no reader). Both degrade branches share one local _degrade(detail).
  • Per-fire config read uses load_config_readonly() with the same guard as the sibling failure_nudge_threshold read.
  • Rationale restated in six places collapsed to the helper docstring; the once-per-process warning is worded as a host-level notice (third commit).

Validation

Check Result
Linux (run_tests.sh on tests/cron/test_restart_safe_worker.py, tests/hermes_cli/test_kanban_gateway_restart_handoff.py, tests/tools/test_process_registry.py) 131 passed, 0 failed, 4 skipped
macOS, same files 112 passed, 0 failed, 23 skipped (was 2 failed at the PR head)
Live probe, Linux, managed + no bus, real helper main: RuntimeError on every dispatch, 0 Popen. Branch: default → 1 external Popen, no systemd-run, one warning across 2 dispatches; require_restart_safe_scope: true → raises, 0 Popen; unmanaged → in_process
Mutation: fix files reverted to main, tests kept 7 red incl. both new invariants
Mutation: warn-once guard removed warn-once test red
ruff, check-windows-footguns.py --all, check_compat_pointers.py, git diff --check clean

Open policy question (unchanged from the original PR)

Default is degrade (cron.require_restart_safe_scope: false), which flips the fail-closed default #101940 introduced. The needs-decision label on #102431 stands; flipping the default is a one-line change in hermes_cli/config_defaults.py if maintainers prefer fail-closed. Kanban is unaffected either way.

@kshitijk4poor kshitijk4poor added comp/cron Cron scheduler and job management type/bug Something isn't working P1 High — major feature broken, no workaround needs-decision Awaiting maintainer decision before any implementation labels Sep 12, 2026
kiwipaulrob and others added 3 commits September 12, 2026 23:07
A systemd-supervised gateway (INVOCATION_ID set) with no user D-Bus
session (containers, minimal LXCs, supervisors without linger) fails
EVERY scheduled job at dispatch: restart_safe_gateway_child_argv()
raises, run_one_job() records a failure, and the only symptom is
silently skipped executions (a missed nightly backup, dead watchdogs,
no alert).

Cron now degrades to a direct external subprocess with a
once-per-process warning instead of raising, unless
cron.require_restart_safe_scope=true (config.yaml, default false)
restores fail-closed. Degraded jobs keep process separation and the
full NousResearch#101940 ownership handoff - only cgroup isolation is lost, so a
mid-job gateway restart kills the worker and the execution ledger
records exactly that.

The dispatch is a GatewayChildDispatch NamedTuple (in_process /
scoped / degraded) so the degraded case can never collapse into the
"not managed, stay in-process" sentinel - the failure mode that would
recreate the restart-interruption edge NousResearch#101940 closed.

Kanban stays fail-closed (require_restart_safe_scope=True at its call
sites): its workers are long-lived agentic runs, so the degrade policy
is limited to bounded cron jobs in this PR.

Addresses the NousResearch#102431 review: the env-var flag became a config key per
AGENTS.md (no new HERMES_* non-secret vars), Kanban keeps fail-closed
instead of updating its tests to a degraded contract, main's
enable-linger remedy message is preserved, and the degrade warning
fires once per process.
Follow-up to the cherry-picked NousResearch#102431 fix, addressing the review findings:

- The two real-helper scheduler tests ran the Linux-only helper unmarked
  and failed on macOS/Windows; the surviving one is now `linux_only`.
- `_warn_scope_degraded_once` used a bare `os.getuid()` that tripped
  scripts/check-windows-footguns.py --all (lint lane red). The remedy text
  is now built once in the helper and passed into the warning, so the
  "scope binary vanished" case no longer warns about a missing D-Bus.
- Tests trimmed to the invariant bar: degraded != in_process and warns
  once (helper level); default config still Popens externally and
  `require_restart_safe_scope: true` raises (scheduler level, real helper).
  Dropped the stubbed duplicate, the standalone config-raise test and the
  in_process half already covered by the existing passthrough test.
- `GatewayChildDispatch.reason` had no reader outside a test; removed.
- Both degrade branches share one local `_degrade(detail)`.
- The per-fire config read uses `load_config_readonly()` (no deepcopy)
  with the same `except Exception` guard as the sibling
  `failure_nudge_threshold` read, so a config error no longer escapes
  the launcher.
- Kanban's no-run-id guard fails closed for any non-`in_process` mode
  instead of matching one enum value.
- Rationale restated in six places collapsed to the helper docstring;
  `cron.require_restart_safe_scope` documented in the cron user guide.
…notice

The warning fires once per process but was prefixed with the first
job's unit suffix, reading as a per-job notice for a host-level
condition. Drop the suffix; the message now says what applies to every
later dispatch.
@kshitijk4poor
kshitijk4poor force-pushed the fix/cron-scope-degrade-salvage-102431 branch from b9691a8 to 78100e2 Compare September 12, 2026 17:38
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) September 12, 2026 17:39
@kshitijk4poor
kshitijk4poor merged commit 1c671be into NousResearch:main Sep 12, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management needs-decision Awaiting maintainer decision before any implementation P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants