openclaw - ✅(Solved) Fix cron: cron add/edit silently allows duplicate name collisions [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#76160Fetched 2026-05-03 04:41:37
View on GitHub
Comments
1
Participants
2
Timeline
19
Reactions
2
Timeline (top)
referenced ×14cross-referenced ×3commented ×1unsubscribed ×1

openclaw cron add (and cron edit) silently accept new jobs whose name collides with an existing job's name. Two distinct rows with identical names land in jobs.json, each with its own id, schedule, enabled, etc. Default cron list only renders one of them when their enabled differ, hiding the divergence from the operator.

Error Message

  • Default: refuse the operation with an actionable error (Error: a cron job named 'foo' already exists (id=…). Use --replace to overwrite, --rename to choose a new name, or 'cron rm' first.).
  • cron add --name <existing> exits non-zero with the actionable error above by default.
  • The agent-facing cron tool returns the same error structure so agents don't trip into silent duplicates.

Root Cause

The older row was disabled and superseded ~2.7h later by a re-add with a different delivery channel. Both lived in jobs.json indefinitely; only the enabled one ran. Behavior was correct here because the disabled state acted as the dedup signal — but if the operator had left both enabled, the job would silently fire twice per cycle with two divergent prompts. There is no UI surface today that exposes that risk.

Fix Action

Fix / Workaround

  • Default: refuse the operation with an actionable error (Error: a cron job named 'foo' already exists (id=…). Use --replace to overwrite, --rename to choose a new name, or 'cron rm' first.).
  • Opt-in override: --replace flag overwrites the existing row in place (preserving id, replacing other fields). Mirrors the semantics of patch-herd-cron.py's byte-equality refresh model.
  • Defensive listing: consider showing duplicate-name groups in cron list --all with a warning marker so operators discover legacy duplicates left over from before the validation lands.

PR fix notes

PR #76180: fix(cron): reject add/update when job name collides with existing job (#76160)

Description (problem / solution / changelog)

Problem

`cron add` and `cron edit` silently accepted new jobs whose `name` collided with an existing job's `name`. Two distinct rows with identical names landed in `jobs.json`, each with its own `id`, `schedule`, and `enabled` state. If both rows were enabled they fired twice per cycle with divergent prompts; if one was disabled, the duplicate was invisible in the default `cron list` output (disabled rows filtered unless `--all`). `cron edit --name <existing>` could also silently rename onto an existing job through the same gap in `ops.update`.

Fixes #76160.

Root cause

`ops.add` called `state.store.jobs.push(job)` with no name uniqueness check.
`ops.update` applied the name patch and replaced the job by ID without checking sibling names.

Changes

`src/cron/service/jobs.ts`

  • `findJobByName(state, name)`: finds an existing job by normalized name (same pattern as `findJobOrThrow` for IDs).

`src/cron/service/ops.ts`

  • `add`: throws before `push` when `findJobByName` returns a match. Error message includes the colliding `id` so operators know what to remove.
  • `update`: throws after `applyJobPatch` when the patched name collides with a different job. Same-job rename (keeping the current name) is allowed.

Test result

`11 passed (11)` in `src/cron/service/ops.test.ts`. 4 new cases:

  • refuses `add` when a job with the same name already exists
  • refuses `add` even when the existing job is disabled
  • refuses `update` rename onto an existing name (different ID)
  • allows `update` to keep the same name on the same job

`oxlint`: 0 warnings, 0 errors on all changed files.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/cron/service/jobs.ts (modified, +4/-0)
  • src/cron/service/ops.test.ts (modified, +64/-1)
  • src/cron/service/ops.ts (modified, +15/-0)
  • src/gateway/server-methods/cron.validation.test.ts (modified, +63/-0)

Code Example

# starting with a clean cron store
openclaw cron add --name foo --message 'a' --interval 30m
openclaw cron add --name foo --message 'b' --interval 30m
openclaw cron list                # may show only one
openclaw cron list --all --json | jq '[.jobs[] | select(.name=="foo")] | length'
# => 2

---

JOB: 'imessage-30min-scan' id=a0394d62-… enabled=False createdAtMs=1776885941338 msg_len=1316
JOB: 'imessage-30min-scan' id=26d0be12-… enabled=True  createdAtMs=1776895555114 msg_len=1289
RAW_BUFFERClick to expand / collapse

Summary

openclaw cron add (and cron edit) silently accept new jobs whose name collides with an existing job's name. Two distinct rows with identical names land in jobs.json, each with its own id, schedule, enabled, etc. Default cron list only renders one of them when their enabled differ, hiding the divergence from the operator.

Repro

# starting with a clean cron store
openclaw cron add --name foo --message 'a' --interval 30m
openclaw cron add --name foo --message 'b' --interval 30m
openclaw cron list                # may show only one
openclaw cron list --all --json | jq '[.jobs[] | select(.name=="foo")] | length'
# => 2

Observed in the wild

Found during a config audit on a production deployment:

JOB: 'imessage-30min-scan' id=a0394d62-… enabled=False createdAtMs=1776885941338 msg_len=1316
JOB: 'imessage-30min-scan' id=26d0be12-… enabled=True  createdAtMs=1776895555114 msg_len=1289

The older row was disabled and superseded ~2.7h later by a re-add with a different delivery channel. Both lived in jobs.json indefinitely; only the enabled one ran. Behavior was correct here because the disabled state acted as the dedup signal — but if the operator had left both enabled, the job would silently fire twice per cycle with two divergent prompts. There is no UI surface today that exposes that risk.

Why this hurts

  1. Silent dual-fire. Two enabled rows with the same name → 2× executions per cycle, 2× cost, divergent prompts (one stale).
  2. Default cron list hides the case. Disabled rows are filtered out unless --all is passed, so an operator looking at the list won't see the duplicate.
  3. Fix-by-name is ambiguous. PR #28791 (fix(cron): allow removing jobs by name in addition to ID) implicitly assumes uniqueness — cron rm <name> resolves only when names are unique. Allowing duplicates via add/edit undermines that assumption.

Proposed UX

cron add and cron edit SHOULD treat name as unique within an agent's job set:

  • Default: refuse the operation with an actionable error (Error: a cron job named 'foo' already exists (id=…). Use --replace to overwrite, --rename to choose a new name, or 'cron rm' first.).
  • Opt-in override: --replace flag overwrites the existing row in place (preserving id, replacing other fields). Mirrors the semantics of patch-herd-cron.py's byte-equality refresh model.
  • Defensive listing: consider showing duplicate-name groups in cron list --all with a warning marker so operators discover legacy duplicates left over from before the validation lands.

The check belongs in the gateway-side cron-add handler (so it covers CLI, the cron agent tool, and any future surfaces) — not just the CLI-side argument parser.

Code pointers (gateway 2026.4.14, hash db2jj6bs2…323493fa)

Bundled output, unminified — easy to map to source:

  • dist/cron-cli-AozygQuG.js — CLI handler (the add / edit entry points). Lines ~380, ~478, ~490 reference payload.lightContext etc.
  • dist/jobs-BuytQ0ly.js — job storage layer. Lines ~616, ~637 mutate the job list. Likely where the uniqueness check belongs.
  • dist/openclaw-tools-DrVjmaoR.js — the agent-facing cron tool (lines ~1238, ~1343, ~1419 touch payload normalization). Should enforce the same rule.

Acceptance

  • cron add --name <existing> exits non-zero with the actionable error above by default.
  • cron add --name <existing> --replace preserves the existing id and overwrites the other fields atomically.
  • cron edit --name <existing> errs the same way if used to rename onto an existing name.
  • The agent-facing cron tool returns the same error structure so agents don't trip into silent duplicates.
  • Tests cover: (a) refuse on collision, (b) --replace overwrites in place, (c) disabled rows participate in the uniqueness check.

Refs

  • markthebest12/openclaw-infra#1305 (where the live duplicate was found and removed).
  • #28791 (cron rm by name — relies on uniqueness).

extent analysis

TL;DR

To prevent silent dual-fire and ensure data consistency, the cron add and cron edit commands should be modified to treat job names as unique within an agent's job set, with options to refuse duplicates, replace existing jobs, or rename new jobs.

Guidance

  • Implement a uniqueness check for job names in the gateway-side cron-add handler to cover all surfaces, including CLI, cron agent tool, and future interfaces.
  • Introduce a --replace flag to overwrite existing jobs with the same name, preserving the id and replacing other fields atomically.
  • Consider displaying duplicate-name groups in cron list --all with a warning marker to help operators discover legacy duplicates.
  • Update the agent-facing cron tool to enforce the same uniqueness rule and return the same error structure as the CLI.

Example

// dist/jobs-BuytQ0ly.js (job storage layer)
function addJob(job) {
  const existingJob = getJobByName(job.name);
  if (existingJob && !job.replace) {
    throw new Error(`A cron job named '${job.name}' already exists (id=${existingJob.id}). Use --replace to overwrite, --rename to choose a new name, or 'cron rm' first.`);
  }
  // ...
}

Notes

The proposed solution assumes that the uniqueness check belongs in the gateway-side cron-add handler. However, the exact implementation details may vary depending on the specific requirements and constraints of the system.

Recommendation

Apply the proposed workaround by implementing the uniqueness check and --replace flag in the cron add and cron edit commands to prevent silent dual-fire and ensure data consistency. This approach ensures that job names are unique within an agent's job set, providing a more robust and reliable cron management system.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix cron: cron add/edit silently allows duplicate name collisions [1 pull requests, 1 comments, 2 participants]