Skip to content

fix(profiles): merge cron/jobs.json per job so update keeps the installer's jobs - #120824

Closed
jonpol01 wants to merge 1 commit into
NousResearch:mainfrom
jonpol01:fix/profile-update-keeps-user-cron-jobs
Closed

jonpol01 wants to merge 1 commit into
NousResearch:mainfrom
jonpol01:fix/profile-update-keeps-user-cron-jobs

Conversation

@jonpol01

Copy link
Copy Markdown

What does this PR do?

This fixes cron jobs in profile distributions:

  • profile update no longer deletes the installer's own jobs. Before, a shipped cron/jobs.json replaced the whole store.
  • Installed jobs are no longer re-armed. profile install now brings shipped jobs in paused, as the Security section already promised.
  • Update keeps each job's state. It refreshes shipped jobs' definitions and keeps whether the user paused or resumed each one.
  • Runtime state is no longer shipped or replaced. That covers locks, ledgers, run output, and hub and curator files inside cron/ and skills/.

Related Issue

Fixes #120823

This follows up #111344, whose tests modelled user jobs as loose cron/*.json files the runtime never reads. #44386 is mostly superseded. #109581 rewrites the copy path without merging cron jobs; this change is local (one table, one merge function, a rel argument), so rebasing either one should be simple.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/profile_distribution.py:
    • _merge_cron_jobs merges cron/jobs.json job by job, keyed on the job id the author's store assigned, which is kept on import so context_from chains resolve. It works under the cron store's own lock, and reads a copy of the shipped file so a local source is never written.
    • A job the profile already has gets the author's definition fields (the create_job arguments) and keeps its own enabled/paused state, run history and failure streak.
    • A job new to the profile arrives paused with no next run.
    • A _MERGED_FILES table routes cron/jobs.json to that merge. Every other owned file keeps the existing replace.
    • _RUNTIME_STATE lists what the runtime itself writes inside cron/ and skills/. Owned-entry listing and _merge_dir skip it, so the installer's live copies are never swapped out. Each entry was checked against the code that writes it.
  • website/docs/user-guide/profile-distributions.md:
    • the Step 3 .gitignore now lists that runtime state (cron/jobs.json stays committed);
    • the ownership table, the update steps and the Security section describe the per-job merge and paused install.
  • Tests: two invariants on the real cron store, with jobs created by create_job as hermes cron add does.
    • test_install_leaves_shipped_cron_jobs_paused_and_not_due: red on main (is_job_runnable is true, and the job fires).
    • test_update_merges_cron_store_per_job: red on main ("the installer's own cron job was deleted by the update").
    • Narrower sabotages (imported jobs left enabled; update re-pausing a resumed job) each redden only their own test.

Trade-offs:

  • origin, deliver and workdir count as the author's definition, as they did under the old wholesale copy.
  • A changed interval schedule isn't re-anchored until its next run.

How to Test

  1. scripts/run_tests.sh tests/hermes_cli/test_profile_distribution.py tests/cron/test_jobs.py: 162 passed. Both new tests fail on main.
  2. Profile-distribution, install-preview, gateway-standalone, prompt-builder and cron-store suites: 8 files, 237 passed, plus 7 more cron tests.
  3. The hunt repro (adapted for paused jobs): after install the job is ('weekly-digest', 'disabled', 'paused') and nothing is due. After profile update, both the shipped job and my-own-reminder are still there.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass. I ran only the related files above through scripts/run_tests.sh, not the full suite.
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

…ller's jobs

The cron runtime keeps every job of a profile in one file, cron/jobs.json,
and the distribution payload treated it as a single root replaced
wholesale. `hermes profile update` therefore deleted every job the
installer had added and re-armed shipped jobs they had paused, and
`profile install` left shipped jobs enabled, so a job that was due in the
author's profile fired on the first tick, contradicting the "not
auto-scheduled" promise in the CLI and docs.

Merge the store job by job, keyed on the id the author's store assigned
(kept on import so context_from chains still resolve). A shipped job the
profile already has gets the author's definition (the create_job fields)
and keeps its enabled/paused state and run history. A job new to the
profile is imported paused with no next_run_at. The installer's own jobs
are left alone. The write goes through the store's lock and save_jobs.

Runtime state inside cron/ and skills/ (locks, ticker markers, the
WAL-mode ledgers, run output, hub, usage and curator state) is no longer
shipped or replaced. The docs' .gitignore guidance now excludes it and
keeps cron/jobs.json.
@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 @jonpol01 — for the report in #120823 and for this first fix.

The fix has landed in #121264 as a salvage stack built on #120910's per-job merge, and it covers everything this PR set out to do:

  • profile update merges cron/jobs.json by job id instead of replacing it, so the installer's own jobs and each job's paused/enabled state survive (import_job_definitions in cron/job_definition.py).
  • profile install brings shipped jobs in paused with no next run.
  • Runtime state under cron/ and root-level hidden entries under skills/ are never copied. We went with the ownership rule from fix(profiles): merge distributed cron jobs without replacing runtime state #120910 (cron/jobs.json is the only distributable cron entry) rather than an enumerated _RUNTIME_STATE table, so there is no list to keep in sync.
  • Your docs points are in: the Step 3 .gitignore now lists cron/*, !cron/jobs.json, skills/.*, and the ownership table, update steps and Security section describe the merge and paused install.
  • The "changed interval isn't re-anchored until its next run" trade-off is gone: a changed schedule goes through the scheduler's own update path.

You are credited as co-author on 68ca2c8.

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