Skip to content

fix(automation): keep completed once tasks completed on schedule re-anchor (#504) - #514

Draft
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-futcca
Draft

fix(automation): keep completed once tasks completed on schedule re-anchor (#504)#514
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-futcca

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #504.

recomputeNextRun (packages/automation/src/task.ts) hard-coded previousRun = undefined when re-seeding next_run_at after a schedule edit. For an interval schedule that's harmless, but for a once schedule last_run_at is the only "already fired" signal — so passing undefined resurrected a finished one-shot: it wrote the original, now-past run_at back into next_run_at, making the task immediately due again on the next tick.

The single caller (automation-routes.ts PATCH handler) fires on any request carrying a schedule key — including a UI that re-sends the unchanged object on save — so editing an unrelated field (rename, model change) would silently re-execute the one-shot prompt's real side effects (backup / report / deploy).

Fix

Anchor on the task's own last_run_at instead of undefined:

setNextRunAt(task, scheduleNextAfter(task.schedule, task.last_run_at, now));
  • Never-run task: last_run_at === undefined, so interval re-anchoring is byte-for-byte unchanged.
  • Completed once: last_run_at is present → scheduleNextAfter returns undefined → the task stays completed.
  • Interval that has run: anchors at last_run_at + interval; the existing skip-forward loop still guarantees a result strictly after now.

Tests

  • New regression test: a completed once stays next_run_at === undefined / not-due after recomputeNextRun.
  • Existing "reseeds an interval schedule for a never-run task" test still passes unchanged.

node --test packages/automation/src/task.test.ts → all 22 tests pass. (The unrelated store.test.ts failure is pre-existing in this environment — @jarvis/store is not installed — and reproduces on the untouched tree.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01ShHwvKnfC9aaPhXxn5kpMp


Generated by Claude Code

…-anchor (#504)

`recomputeNextRun` hard-coded `previousRun = undefined` when re-seeding
`next_run_at`. For an `interval` schedule that's harmless, but for a `once`
schedule `last_run_at` is the only "already fired" signal, so passing
`undefined` resurrected a finished one-shot: it wrote the original, now-past
`run_at` back into `next_run_at`, making the task immediately due again on the
next tick.

The caller fires on any PATCH carrying a `schedule` key — including a UI that
re-sends the unchanged object on save — so editing an unrelated field (rename,
model change) would silently re-execute the one-shot prompt's real side effects
(backup / report / deploy).

Anchor on the task's own `last_run_at` instead. A never-run task still has
`last_run_at === undefined`, so interval re-anchoring is unchanged; a completed
`once` now stays completed. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShHwvKnfC9aaPhXxn5kpMp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants