dsh-ops-cron/docs/cron_reschedule原语详设.md
oliver 89ab5f59af Add cron_reschedule so pending one-shots can be moved without delete/recreate.
Keeps retrigger for consumed jobs only; agents can advance or delay nextRunAt safely.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 19:57:22 +08:00

4.7 KiB
Raw Blame History

cron_reschedule 原语详设

状态:已在本包落地(rescheduleJob + cron_reschedule + POST /jobs/:id/reschedule)
包:dsh-ops-cron(本仓库可读源码,非混淆服务)
背景:一次性 at 任务在仍有 nextRunAt(pending)时,cron_retrigger 会拒绝;Agent 只能删重建,体验差。


1. 结论

改 pending 可以做,且应做。
语义是「把预定的那一次触发改期/提前」,不是「再开一次并发 run」。比放开 cron_retrigger 更精确、更安全。

方案 做法 评价
A 改 cron_retrigger:有 pending 时也接受,行为=抢占改期 可行,但混淆「已消费再触发」与「未到期改期」
B(推荐) 新增 cron_reschedule 不碰 retrigger 语义;最小侵入、向后兼容

2. 现状(代码事实)

  • isRetriggerable / retriggerJob:one-shot 仅当 nextRunAt == null 且 lastStatus 终态才可 retrigger(lib/monitor.js、lib/host.js)。
  • pending 拒绝文案已写明:one-shot still has a pending next run; wait or edit the schedule instead of retrigger。
  • 侧栏 / HTTP 已能改期:PATCH /jobs/:id → updateJob({ schedule });若 patch 了 schedule,会经 createJobRecord 重算 nextRunAt(host.js 约 698–700 行:仅当未改 schedule/enabled 才保留旧 next)。
  • 缺口:Agent 工具面没有 cron_update / cron_reschedule,只有 create / list / pause / resume / retrigger / delete。

因此:不必「热改运行中混淆服务」;在本包加工具(可选再加 host 薄封装)即可落地。


3. 推荐 API:cron_reschedule

3.1 签名

cron_reschedule({
  id | task_id: string,          // 必填
  after_minutes?: number,        // ≥0;与 at 二选一(优先 after_minutes)
  at?: string,                   // ISO 或与 cron_create 一致的本地时间语义
  timezone?: string              // 可选;默认沿用 job.schedule.timezone
})

3.2 行为

  1. 仅 schedule.kind === 'at'。
  2. 仅当 仍有 pending next(nextRunAt != null)且 无 active run(queued/running)。
  3. 计算新触发时刻 T(after_minutes=0 → 立即 due;>0 → now+N;at → 解析后的绝对时间)。
  4. T 必须 ≥ now(允许 0 表示立刻进入 due,由现有 tick/run-now 路径消费;不要另开第二条 pending)。
  5. 写回:
    • schedule = { kind: 'at', at: ISO(T), timezone }
    • nextRunAt = T
    • enabled 保持不变(paused 允许改 next,不隐式 resume)
  6. 不调用 dispatchRun(除非产品明确要 fire_now=true;默认不火,交给调度器)。
  7. 返回 { ok, job, nextRunAt, previousNextRunAt }。

3.3 拒绝条件(稳定 error.code)

条件 code
非 one-shot INVALID_RESCHEDULE
nextRunAt == null(已消费) INVALID_RESCHEDULE → 提示改用 cron_retrigger
已有 queued/running ALREADY_RUNNING
时间非法 / 过去 INVALID_RESCHEDULE
周期 cron INVALID_RESCHEDULE(周期改期另议,勿混进本原语)

3.4 与 retrigger 分工(写进 tool description + skill)

状态 用哪个
one-shot 已跑完(next=n/a,retriggerable=true) cron_retrigger
one-shot 仍在等(有 nextRunAt) cron_reschedule
recurring 立刻多跑一次 cron_retrigger(不改 cron 表达式)

4. 实现落点(本仓库)

  1. lib/host.js:新增 rescheduleJob(jobId, opts, identity)(或在 updateJob 外包一层校验);可复用 scheduleFromArgs / nextFire。
  2. lib/tools.js:注册 cron_reschedule;更新 scheduled-tasks skill 文案(禁止删重建来改期)。
  3. HTTP(可选):POST /jobs/:id/reschedule,与 PATCH 并存;Agent 走工具即可。
  4. 测试:host.test.js / tools.test.js
    • pending → after_minutes=1 → nextRunAt 前移
    • pending + active run → 拒绝
    • consumed → 拒绝并指向 retrigger
    • cron kind → 拒绝
    • paused pending → 只改 next,仍 enabled=false

工作量小:调度内核已支持「改 schedule ⇒ 新 next」;主要是 Agent 可发现原语 + 边界校验。


5. 明确不做

  • 不对 pending one-shot 放开无条件 cron_retrigger(避免与「已消费再触发」心智冲突)。
  • 不引入第二条并发 pending。
  • 不把 reschedule 做成隐式 start(paused 保持 paused)。

6. 临时绕过(原语未上线前)

  • 人:侧栏编辑任务时间 → Save(已走 PATCH)。
  • Agent:无工具时只能 delete + create(应在 skill 里标明为 workaround,待 cron_reschedule 替换)。