Skip to content

fix(profiles): merge distributed cron jobs without replacing runtime state - #120910

Closed
JoaoMarcos44 wants to merge 2 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/profile-cron-store-120823
Closed

JoaoMarcos44 wants to merge 2 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/profile-cron-store-120823

Conversation

@JoaoMarcos44

@JoaoMarcos44 JoaoMarcos44 commented Sep 24, 2026 •

Copy link
Copy Markdown

Summary

Fixes #120823.

Profile distributions currently treat cron/ like a directory of independent authored files, but Hermes cron has one canonical multi-record store: cron/jobs.json. Replacing that file on install/update replaces the installing profile's scheduler state wholesale:

  • local jobs disappear on profile update;
  • a user's pause/resume choice for shipped jobs is lost;
  • newly shipped jobs inherit the author's runnable state;
  • runtime siblings under cron/ can be copied as if they were authored distribution content.

This patch makes the ownership boundary explicit and small:

A profile distribution may own cron definitions in cron/jobs.json. Every other entry under cron/ is runtime state owned by the installing profile.

Root cause

hermes_cli/profile_distribution.py::_merge_dir currently treats every file under a distribution-owned directory as an independent replaceable root.

That assumption works for skill directories, but not for cron: cron/jobs.json is the shared store for all jobs in the profile. Wholesale replacement therefore crosses record ownership boundaries.

The same generic directory merge also has no distinction between authored skills and root-level hidden skill bookkeeping such as .hub, .usage.json, curator state, manifests, locks, and archives.

Fix

Canonical cron ownership

  • cron/jobs.json is the only distributable entry under cron/.
  • Other current or future cron siblings are ignored automatically; there is no runtime-file blacklist to keep in sync.
  • An explicitly allowlisted non-store cron path is ignored for the same reason.

Job-by-job merge

The shipped store is read through the real cron.jobs loader, then merged by stable job id. Cron itself owns the authored-field schema via JOB_DEFINITION_FIELDS, so profile-distribution code does not duplicate the job record contract:

  • local-only jobs are retained unchanged;
  • existing shipped jobs receive updated definition fields while keeping local scheduler state;
  • repeat budget (times) comes from the author while local progress (completed) survives;
  • newly shipped jobs are imported disabled/paused with next_run_at=None;
  • when an authored schedule changes, derived next_run_at is re-anchored through cron's existing schedule-update logic instead of carrying an instant derived from the old schedule.

The merge uses the cron store's own lock and save path rather than writing jobs.json directly.

Skills runtime boundary

At the root of skills/, hidden entries are Hermes bookkeeping and remain local. Hidden files inside an authored skill directory are still copied, so the rule does not alter skill package contents.

Why this is intentionally structural

#120824 addresses the same issue and enumerates the current runtime files under cron/ and skills/.

This alternative avoids a list that can become stale when the scheduler gains another DB, lock, ledger, output directory, or sidecar. The ownership rule follows the actual runtime architecture: the cron distribution surface is the job store, not the scheduler's working directory.

Regression coverage

tests/hermes_cli/test_profile_distribution.py now uses the real cron store rather than the old loose cron/*.json fixture model.

Coverage pins:

  1. Install safety

    • source job is intentionally due/runnable;
    • installed job is paused and not due;
    • arbitrary future cron runtime state is not copied;
    • root hidden skills runtime state is not copied;
    • an authored hidden file inside a real skill still is copied.
  2. Update preservation

    • shipped job is resumed locally;
    • installer creates and pauses a separate local job;
    • upstream changes the shipped job definition;
    • update keeps the local job and its pause reason;
    • update refreshes the shipped definition without re-pausing/resuming it.
  3. Allowlist parity

    • nested skill paths still work;
    • the canonical cron/jobs.json path can be explicitly allowlisted.

Scope

Files changed:

  • cron/jobs.py
  • hermes_cli/profile_distribution.py
  • tests/hermes_cli/test_profile_distribution.py
  • website/docs/user-guide/profile-distributions.md

No scheduler execution logic, database schema, delivery behavior, or CLI syntax changes.

Verification

Built from current upstream main at 068db016fbfb3e9b44169de154781908f91bceae.

No local checkout/worktree was used. At head b8b50f7521e10bbb6b66b1931965ce3383faeb90, GitHub created CI, Nix flake check, and Docker Build, Test, and Publish, but all three concluded action_required before creating any jobs (0 jobs). No remote test suite has executed yet, so this PR does not claim green CI.

Infographic

file_00000000f1c0820ea6b1ccd1ff262cd9.png

@JoaoMarcos44
JoaoMarcos44 marked this pull request as ready for review September 24, 2026 02:09
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management area/profiles Multi-profile isolation, HERMES_HOME scoping sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Sep 24, 2026
@kshitijk4poor

Copy link
Copy Markdown

Thanks @JoaoMarcos44 — this PR is the base of the salvage.

Your two commits are carried as-is with your authorship in #121264:

  • 99b7078 fix(profiles): merge distributed cron jobs safely
  • 06c3062 test(profiles): cover cron distribution ownership

On top of them the stack adds:

  • 68ca2c8 moves JOB_DEFINITION_FIELDS / merge_job_definition into a new cron/job_definition.py so cron/jobs.py is unchanged.
  • 5ae3909 rejects a shipped job the scheduler cannot take (past one-shot, unparseable string) with a job-labelled DistributionError before any file is replaced; previously the update stopped halfway with SOUL.md already overwritten.
  • a49f13d turns a corrupt target jobs.json into a DistributionError instead of a traceback.
  • d9a6a13 / f17b78f move the store-import loop into cron/job_definition.py and gate the schedule re-anchor on is_job_runnable.

Tests: tests/hermes_cli/test_profile_distribution.py + tests/cron, 1412 passed / 10 skipped.

Merged as b0aefcce70. Closing in favour of #121264.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management P1 High — major feature broken, no workaround sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: profile update deletes the user's own cron jobs, and profile install leaves shipped jobs running

3 participants