dsh-cron
已验证@aiwayds/dsh-cron · v0.5.0 · MIT
dsh plugin: cron scheduling — bounded tasks with calendar & interval rules, delivered to live agents
安装
dsh plugin add @aiwayds/dsh-cron 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
说明文档
@aiwayds/dsh-cron
Cron scheduling for the DeepSeek Harness as an independent plugin: schedule prompts on standard cron expressions or fixed intervals and have them delivered to live agents — across TUI, web, and feishu surfaces.
Requires dsh >= 0.2.0-rc.2 — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the latest/next dist-tags at runtime). The alpha line is no longer supported.
Design docs: CONTEXT.md (glossary) and docs/adr/ (decisions).
Why not dsh-schedule?
@deepseek-ai/dsh-schedule provides session-scoped reminders (after / at / every_seconds ≥ 5min) that only fire while the original session stays live, and its protocol explicitly excludes cron expressions. dsh-cron is the complement: profile-anchored recurring tasks — a fire lands in whichever live root agent exists, regardless of which surface the session belongs to.
As of dsh 0.2.0, the official automation schedule ships as an OPTIONAL_BUNDLE that is disabled by default — after upgrading, existing official scheduled tasks silently stop firing until you manually opt in (and the official time-context feature goes dark with it). dsh-cron is unaffected by that switch: it is an independent, always-resident scheduling plugin that registers its own cordis plugin and clock, with no dependency on the official bundle — out of the box on every profile it is installed in, no opt-in required. If you relied on official scheduled tasks and do not want to hunt for a hidden settings toggle after each host upgrade, dsh-cron is the always-on alternative; the two can also coexist, since their scopes do not overlap (session-bound reminders vs profile-anchored cron).
Core properties
- No infinite cron (ADR 0006): every recurring task carries a mandatory validity window (
max_duration_secondsorend_at), capped at one year. On expiry the task delivers one terminalexpiredfire and archives itself. One-shot tasks self-archive after their single fire. - Two rule shapes (ADR 0007):
cron(5-field calendar expression, host-local time) orevery_seconds(interval ≥ 60s, anchored atstart_at, first fire one full interval in). - The plugin owns the clock (ADR 0007): the model only expresses intent (
every_seconds: 600,end_at: "2026-12-31T00:00:00Z"); all time math happens in the plugin and tool results echo absolute ISO times (now,next_fire). The model never needs to know the current time. - Per-task delivery policy (ADR 0003):
followup(default) queues a new turn via the agent's idle phase — a busy target waits and coalesces overdue occurrences into one fire annotated withcoalesced_count.steersubmits throughagent.steer()and lands at the nearest step boundary, mid-turn — the watchdog option. - Two execution modes (ADR 0005):
sub-agent(default) — the framing instructs the model to spawn an isolated background worker, then callcron_reportto backfill status/summary into the fire record;self— handled in the target conversation itself, best-effort reporting. - Missed occurrences are skipped, never delivered late (ADR 0002): if the profile was down or no agent was live when an occurrence came due, it is logged and settled. A one-shot missed in downtime archives as
missed. Cron means "execute at time X"; dsh-cron does not resurrect stale work. - Full traceability (Q3): every delivered fire produces a durable
FireRecord(due time, policy, mode, coalesced count, status, summary). Records ride with the task, capped at 7 per task (configurable); ended tasks move to_history.json(cap 50). - Tools for every agent (ADR 0006):
cron_create/cron_list/cron_delete/cron_reportare registered on all runtime agents — root or sub-agent — and every task recordscreatedBy.
Agent usage (primary)
The LLM calls the tools directly. Humans just talk:
- "每 10 分钟检查一次 CI run 1234,最多查两小时,成功或失败就停" →
cron_create({ every_seconds: 600, prompt: "检查 CI run 1234,终态则 cron_delete 本任务并汇报", max_duration_seconds: 7200 }) - "工作日每天早上 9 点生成周报,到年底为止" →
cron_create({ cron: "0 9 * * 1-5", prompt: "生成本周周报", end_at: "2026-12-31T23:59:59+08:00" }) - "每小时看一眼磁盘,如果超过 90% 立刻插进来提醒我" →
cron_create({ cron: "0 * * * *", prompt: "检查磁盘使用率", delivery_policy: "steer", execution_mode: "self", max_duration_seconds: 86400 })
Tool reference
| Tool | Purpose |
|---|---|
cron_create |
Create a task. Exactly one rule (cron | every_seconds); recurring tasks require exactly one window bound (max_duration_seconds | end_at). |
cron_list |
Active tasks with next-fire times and retained fire records. |
cron_delete |
Delete and archive (status cancelled). |
cron_report |
Backfill a fire's outcome (completed/failed + summary); one-shots archive on report. |
Cron syntax
Standard 5 fields: minute hour day-of-month month day-of-week, with lists (1,3,5), ranges (1-5), steps (*/2), and month/day names (jan, mon). Evaluated in the host's local time zone (a per-task IANA time_zone is a planned fast-follow — see ADR 0009).
Human usage
/cron list
/cron create "*/10 * * * *" "检查 CI run 1234,终态则删除本任务" --for=7200
/cron create "0 9 * * 1-5" "生成周报" --until=2026-12-31T23:59:59+08:00
/cron delete a1b2c3d4
/cron fires a1b2c3d4
/cron history 20
Persistence
Profile-level, not per-session (ADR 0001):
$DSH_HOME/storages/cron/<8-hex-id>.json # active tasks with their fire records
$DSH_HOME/storages/cron/_history.json # capped archive of ended tasks
Atomic writes (tmp + fsync + rename); corrupt files are skipped, never fatal.
Installation
dsh plugin --profile tui add @aiwayds/dsh-cron
Then add the package to the profile's dsh.profile.bundles (after @deepseek-ai/dsh-base) and restart the profile.
Uninstall
dsh plugin --profile tui remove @aiwayds/dsh-cron
The host reconciles the profile automatically: the dsh.profile.bundles entry is spliced and the plugin's patch layer drops — schedules silently stop firing.
What stays on disk, kept on purpose (deleting data is destructive, and a reinstall rehydrates it):
<dsh home>/storages/cron/<id>.json— active tasks with their fire records, including the schedules' validity windows.<dsh home>/storages/cron/_history.json— the capped archive of ended tasks.
Reinstall semantics follow ADR 0002: occurrences that came due while the plugin was away are skipped and logged, never delivered late — a one-shot missed in downtime is archived as missed. Cron means "execute at time X"; the plugin does not resurrect stale work after a gap.
The cron skill copied to $DSH_HOME/skills/cron (see Skill) is a plain file copy and survives removal — delete it by hand if unwanted. To purge the task data too:
rm -r ~/.dsh/storages/cron
Skill (model guidance)
The npm tarball ships a cron skill (skill/cron/SKILL.md) that teaches the model when and how to use the tools (rule selection, window thinking, the deploy-monitor and watchdog patterns). Install it by copying to the dsh skill root:
mkdir -p $DSH_HOME/skills && cp -r <package>/skill/cron $DSH_HOME/skills/
Settings (dsh-cron entry in the profile patch)
Since dsh 0.1.7 the legacy settings.yaml is imported once at boot and renamed
settings.yaml.imported; plugin settings live in the profile patch
(~/.dsh/profiles/<profile>/cordis.patch.yml) under the dsh-cron entry. All
four keys are volatile (settings-page editable, live where the engine reads
them per tick); storageDir and tickIntervalMs are read at plugin mount, so
changes there apply after a restart.
| Key | Default | Meaning |
|---|---|---|
fireHistoryLimit |
7 | Fire records kept per task (oldest evicted). |
historyLimit |
50 | Archived tasks kept in _history.json. |
tickIntervalMs |
15000 | Scheduler tick period. |
storageDir |
— | Storage override; empty = <dsh home>/storages/cron. |
Upgrading from a pre-0.1.7 install: a top-level cron: section in the old
settings.yaml (which the host renames to settings.yaml.imported after its
one-shot import — the section name does not match this plugin's entry id) is
migrated ONCE at the next plugin boot. Keys whose legacy value differs from the
current effective value are written into the dsh-cron entry automatically;
the pass is recorded in the audit marker
<dsh home>/storages/dsh-cron/legacy-import.json (a present marker means the
migration never re-runs; a failed write retries on the next boot).
Limitations
- Fires need a live profile: dsh-cron is an in-process plugin by design (ADR 0001; an MCP server cannot push). Nothing fires while dsh is not running, and downtime-accrued occurrences are skipped by policy.
- Host-local calendar time: calendar rules read the host clock's zone; containers in UTC will shift wall-clock schedules (ADR 0009).
self-mode outcomes are best-effort: nothing can force the model to callcron_report; sub-agent mode is the traceable default.- One steer per tick: while a steer sits unconsumed in an inbox, later occurrences still submit once per tick rather than collapsing (dsh's inbox cannot be introspected from a plugin).
Architecture
src/
index.ts plugin entry: settings, agent/created mount, /cron, tick loop
scheduler.ts dsh-free tick engine (fires, coalescing, skip, expiry)
rule.ts selector + validity-window validation, occurrence math
cron-expr.ts 5-field cron parser + next-fire computation
framing.ts [CRON FIRE] model framing (injection-resistant)
tools.ts cron_create / cron_list / cron_delete / cron_report
store.ts per-id JSON task store + capped history (atomic writes)
legacy-import.ts one-time `cron:` section migration (0.1.5 → 0.1.7) + audit marker
types.ts data model and clock seam
paths.ts dsh home resolution
test/ unit suites over the compiled lib (fake clocks/agents)
scripts/
smoke-boot.mjs real-host smoke: pack → scratch profile → boot
e2e/ podman suite: real dsh TUI in tmux driven against a scripted mock LLM
E2E
e2e/run-e2e.sh builds a container image (Ubuntu + Node + a global dsh install + the packed plugin tarball) and drives the real TUI in tmux against an in-container mock OpenAI-compatible LLM — no credentials, fully deterministic, the same suite that gates CI. The scenarios exercise the whole chain end to end:
- profile assembly — the plugin composes into the real dsh bundle tree
- TUI boot against the scripted provider
- self-mode chain — the model creates a task, the scheduler fires after 60s, the
[CRON FIRE]framing lands in a live turn,cron_reportbackfills the record, the store assertsstatus: completed - sub-agent chain — the fire spawns a real dsh subagent whose
cron_reportis backfilled by the child - commands & expiry —
/cron list//cron help, the window closes, the task archives itself asexpired,/cron deletearchives ascancelled
./e2e/run-e2e.sh # dev machine (uses registry mirrors)
CLEAN_NETWORK=1 ./e2e/run-e2e.sh # CI-like network (official endpoints)
Ported from @aiwayds/pi-kimi-cron (itself ported from Kimi Code's cron module), redesigned for dsh's runtime — the deltas are all recorded in docs/adr/.