Skip to content

fix(cron): stop one-shot At jobs from re-firing every poll - #5039

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
mysma-9403:fix/cron-at-oneshot-refire
Jul 23, 2026
Merged

fix(cron): stop one-shot At jobs from re-firing every poll#5039
senamakel merged 1 commit into
tinyhumansai:mainfrom
mysma-9403:fix/cron-at-oneshot-refire

Conversation

@mysma-9403

@mysma-9403 mysma-9403 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix a runaway-execution bug: one-shot Schedule::At cron jobs re-fire on every poll interval forever when delete_after_run is not set.
  • Treat any At job as inherently one-shot in the scheduler, so it terminates after a single run regardless of the flag or entry point (agent and shell RPC).
  • Add a regression test asserting an At job with delete_after_run=false is disabled after one run and is never re-selected by due_jobs.

Problem

Schedule::At is a fixed instant. When such a job runs, persist_job_result decides whether to finish it (one-shot) or reschedule it:

  • is_one_shot_auto_delete(job) is job.delete_after_run && matches!(schedule, At).
  • The agent tool defaults delete_after_run to matches!(schedule, At{..}) (tools/add.rs), so tool-created At jobs are correctly one-shot.
  • The RPC handler defaults it to false (schemas.rs handle_add), and add_shell_job never sets it at all (column default 0).

So an At job created over RPC is not is_one_shot_auto_delete, and falls through to reschedule_after_run, which writes next_run = next_run_for_schedule(&At, now) = the (now past) instant. due_jobs selects enabled = 1 AND next_run <= now, so the past at stays perpetually due and the job re-executes on every poll — burning model cost and repeating external side effects for agent jobs.

Solution

Fix at the scheduler rather than mirroring the RPC default (a default-mirror would miss add_shell_job). A fixed-instant job can never have a legitimate future occurrence, so terminate every At job after a single run:

  • Auto-delete job that succeeded → removed (unchanged).
  • Every other At job → kept disabled (enabled = false) with its last run recorded, so history stays inspectable and it never re-fires.
  • Non-At schedules are unaffected — they still reschedule_after_run.

Inside the new At branch, is_one_shot_auto_delete(job) reduces to job.delete_after_run, so the existing helper and its unit tests remain valid.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — persist_job_result_disables_at_job_without_delete_flag covers the previously-buggy delete_after_run=false path; existing ..._success_deletes_one_shot / ..._failure_disables_one_shot cover the auto-delete arms, which now route through the same At branch.
  • Diff coverage ≥ 80% — the changed scheduler lines (the At branch: remove_job, record_last_run + disable, and the guard) are all exercised by the three one-shot tests. cargo test -p openhuman --lib cron::scheduler passes (one unrelated pre-existing failure noted under Impact).
  • Coverage matrix updated — N/A: behaviour-only bug fix, no new/renamed feature.
  • All affected feature IDs listed under ## RelatedN/A: no matrix feature touched.
  • No new external network dependencies introduced.
  • Manual smoke checklist updated — N/A: does not touch a release-cut surface.
  • Linked issue closed via Closes #NNN — no existing issue; this was found by inspection.

Impact

  • CLI/desktop cron: fixes unbounded re-execution of At jobs created via cron.add RPC (agent and shell). No migration; existing rescheduled-to-past At rows stop re-firing the next time they run (they get disabled) — or can be deleted normally.
  • No performance or security implications beyond stopping the wasteful re-runs.
  • Note: cargo test -p openhuman --lib cron::scheduler shows one pre-existing, unrelated failure — run_agent_job_returns_error_without_provider_key overflows the debug thread stack (deep agent async state machine); it reproduces identically on clean main with this change stashed, and does not touch the scheduler reschedule path.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:

AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/cron-at-oneshot-refire
  • Commit SHA: 90c1a16e8

Validation Run

  • pnpm --filter openhuman-app format:check — N/A (no frontend change)
  • pnpm typecheck — N/A (no frontend change)
  • Focused tests: cargo test -p openhuman --lib cron::scheduler (new + one-shot family pass)
  • Rust fmt/check (if changed): cargo fmt
  • Tauri fmt/check (if changed): N/A (core-only change)

Behavior Changes

  • Intended behavior change: At jobs no longer reschedule after running; they terminate (remove-on-success for auto-delete, else disable).
  • User-visible effect: a one-time scheduled job runs once instead of repeating forever.

Parity Contract

  • Legacy behavior preserved: auto-delete-on-success and disable-on-failure for delete_after_run=true At jobs; all non-At schedules reschedule exactly as before.
  • Guard/fallback/dispatch parity checks: is_one_shot_auto_delete semantics unchanged; the new branch is gated strictly on matches!(schedule, At{..}).

@mysma-9403
mysma-9403 requested a review from a team July 18, 2026 12:34
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

At-scheduled cron jobs are now terminated after one execution even when automatic deletion is disabled. Retained jobs record their result, become disabled, and are excluded from later due-job selection. A regression test covers this behavior.

Changes

Cron At job lifecycle

Layer / File(s) Summary
Persist and validate one-shot At jobs
src/openhuman/cron/scheduler.rs, src/openhuman/cron/scheduler_tests.rs
persist_job_result disables or removes completed Schedule::At jobs, and the regression test verifies retained jobs are not selected again.
Estimated code review effort: 2 (Simple) ~10 minutes

Suggested labels: bug

Suggested reviewers: oxoxdev, codeghost21

Poem

A bunny found a timer run,
One hop, then no repeats begun.
Kept the tale, but paused the chore,
Past-due hops won’t happen more.
Wiggle ears— the bug is done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: one-shot At cron jobs no longer re-fire on every poll.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A Schedule::At job created via the RPC handler defaults delete_after_run to
false (schemas.rs handle_add), and add_shell_job never sets it at all, so such
jobs are not is_one_shot_auto_delete. persist_job_result then routes them
through reschedule_after_run, which writes next_run = next_run_for_schedule(At,
now) = the fixed (now past) instant. due_jobs selects enabled=1 AND
next_run<=now, so the past 'at' stays perpetually due and the job re-executes
on every poll interval forever — burning model cost and repeating external
effects for agent jobs.

Fix at the scheduler: a fixed-instant (At) job is inherently one-shot, so
terminate it after a single run regardless of delete_after_run. An auto-delete
job that succeeded is still removed; every other At job is kept disabled so its
run history stays inspectable. This covers both the agent and shell RPC entry
points; the narrower schemas.rs default-mirror would miss add_shell_job.

Test: persist_job_result_disables_at_job_without_delete_flag asserts an At job
with delete_after_run=false is disabled after one run and no longer returned by
due_jobs.
@senamakel
senamakel force-pushed the fix/cron-at-oneshot-refire branch from 90c1a16 to 8c83b9e Compare July 23, 2026 13:33
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a runaway-execution bug where Schedule::At cron jobs created via RPC (with delete_after_run = false) would re-fire on every scheduler poll after their scheduled instant passed. The root cause was that reschedule_after_run writes next_run = at for an At schedule, which is immediately in the past, so due_jobs keeps selecting the row forever.

  • persist_job_result now handles all Schedule::At jobs in a dedicated branch: successful auto-delete jobs are removed, and every other At job is disabled (not rescheduled), preventing re-execution while keeping the run history intact.
  • The success flag is now correctly forwarded to record_last_run instead of being hardcoded to false, so last_status accurately reflects the actual run outcome.
  • A regression test (persist_job_result_disables_at_job_without_delete_flag) covers the previously-buggy delete_after_run = false path and confirms the job is both disabled and absent from due_jobs after one run.

Confidence Score: 4/5

Safe to merge; the fix correctly terminates all At jobs after one run and is well-tested. One error path in the auto-delete branch can leave a job enabled and re-firing if the DB delete fails.

The core fix and its tests are sound. The one gap is that when remove_job fails for a successful auto-delete At job, no fallback disable is applied, so the job re-enters the perpetually-due state the PR is designed to prevent.

src/openhuman/cron/scheduler.rs — the remove_job failure path in the new At branch.

Important Files Changed

Filename Overview
src/openhuman/cron/scheduler.rs Core fix: persist_job_result now gates on Schedule::At before is_one_shot_auto_delete, correctly terminating all fixed-instant jobs after one run. If remove_job fails for an auto-delete-success At job, the job is neither removed nor disabled and can still re-fire — a minor gap worth a fallback disable.
src/openhuman/cron/scheduler_tests.rs New regression test covers the previously-buggy delete_after_run=false At path; checks both the enabled state and due_jobs exclusion after a single run. Existing one-shot tests continue to exercise the auto-delete branches through the refactored At block.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[persist_job_result called] --> B{Schedule::At?}
    B -- No --> C[reschedule_after_run\nnext_run = next occurrence]
    B -- Yes --> D{is_one_shot_auto_delete\nAND success?}
    D -- Yes --> E[remove_job\ndelete row]
    D -- No --> F[record_last_run\nsuccess flag passed]
    F --> G[update_job\nenabled = false]
    E -- remove_job fails --> H[warn log\njob stays enabled\npotential re-fire]
    G -- update_job fails --> I[warn log\njob stays enabled\npotential re-fire]
    E --> J[return success]
    F --> J
    C --> J
Loading

Reviews (1): Last reviewed commit: "fix(cron): stop one-shot At jobs from re..." | Re-trigger Greptile

Comment on lines +1198 to 1202
if is_one_shot_auto_delete(job) && success {
if let Err(e) = remove_job(config, &job.id) {
tracing::warn!("Failed to remove one-shot cron job after success: {e}");
}
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 If remove_job fails, the At job is neither removed nor disabled — its next_run is still the past instant, so it remains perpetually due and will re-fire on the next poll, exactly the bug this PR is fixing. Adding a fallback update_job(enabled: false) after the warn closes this gap.

Suggested change
if is_one_shot_auto_delete(job) && success {
if let Err(e) = remove_job(config, &job.id) {
tracing::warn!("Failed to remove one-shot cron job after success: {e}");
}
} else {
if is_one_shot_auto_delete(job) && success {
if let Err(e) = remove_job(config, &job.id) {
tracing::warn!("Failed to remove one-shot cron job after success: {e}");
// Fallback: disable so the job does not re-fire on the next poll.
let _ = update_job(
config,
&job.id,
CronJobPatch {
enabled: Some(false),
..CronJobPatch::default()
},
);
}
} else {

@senamakel
senamakel merged commit d465096 into tinyhumansai:main Jul 23, 2026
21 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants