squirrel20/dsh-cron
Unattended scheduled jobs for the DeepSeek Harness (dsh): agent/command tasks on cron schedules
Project Overview项目介绍
dsh-cron is an unattended scheduled-jobs plugin for DeepSeek Harness. It runs agent tasks (one-shot agents through the dsh toolchain) or command tasks on cron expressions, fixed intervals, or one-time instants. Use it for host-side automation that must fire while the process is up and is decoupled from any interactive session. The sidebar lists jobs, run history, and replay; the modal configures trigger, task, working directory, timeout, and overlap/misfire policies. Note: dispatch is at-most-once per occurrence, missed fires are not replayed, and runs only execute while the process is running.
dsh-cron 是 DeepSeek Harness 的无人值守定时任务插件,支持 cron 表达式、固定间隔和单次时刻三种触发方式,可执行 agent 任务(通过 dsh 工具链运行提示)或直接运行脚本命令。提供侧边栏任务列表、运行历史回放和创建/编辑弹窗。适用需要在进程运行期间自动执行后台任务、且与会话解耦的场景。注意:每次触发最多执行一次,错过不会逐条补偿;仅当进程在线时才会触发;agent 运行会注入禁用提问的 [CRON RUN] 系统提示。
请帮我了解并安装插件:【dsh-cron】【https://github.com/squirrel20/dsh-cron】
Send this message to DSH in your current session. CLI install commands may not be accurate across systems — DSH will figure it out for you.把上面这条消息直接发给当前会话里的 DSH,让它帮你了解并安装。安装命令不一定准确,发给 DSH 更稳。
Or use CLI install (for developers)或使用命令行安装(适合开发者)
CLI Install命令行安装
dsh plugin --profile web add https://github.com/squirrel20/dsh-cron/releases/latest/download/dsh-cron.tgz
把 squirrel20/dsh-cron 加入你的 DSH 配置(web profile)即可启用。
READMEREADME
dsh-cron
English | 简体中文
Unattended scheduled-jobs plugin for DeepSeek Harness (dsh): run agent tasks (spawn a one-shot agent to execute a prompt through the full dsh toolchain) or command tasks (run a script directly) on a cron expression, a fixed interval, or a one-time instant. Complementary to @deepseek-ai/dsh-schedule — that one is persistent in-session reminders, this one is a host-side job scheduler: jobs belong to no interactive session and fire automatically while the process is up.

UI
- Sidebar section: status dot (last result) + next-trigger time, live elapsed timer while running; rows expand into run history; clicking an agent run jumps straight to that run's full session, and clicking a command run opens a run-detail page over the center column (status, duration, exit code, command, output tail).
- Create / edit modal: trigger presets (hourly / daily / weekdays / weekly, with cron expression / interval / one-shot tucked into a custom tier), task kind, mode / permission / model knobs (blank = inherit defaults), working directory, timeout, and overlap / misfire policies — all on one screen; the time zone is taken silently from the browser (edits keep the job's own).
| Create a job | Row actions |
|---|---|
![]() |
![]() |
Features
- Three trigger kinds:
cron(5-field expression + explicit IANAtimeZone; the process time zone is never consulted),everySeconds(anchor-aligned interval, 60s minimum),at(one-time RFC 3339 instant; a Z or numeric offset is required). - Three ways to declare a job: profile config (declarative, versioned with the profile), the runtime overlay (
+in the sidebar, orcron_createfrom a session — persisted as "manual" jobs), and other plugins through thecronservice (ctx.cron.registerJob, see Adding jobs from a plugin). - Three task kinds:
agent(create a one-shot agent viactx.agents.create, submit the prompt, wait for quiescence, take the last assistant message as the summary, dispose to finish — i.e. the dsh-headless one-shot recipe);command(spawn a child process, record exit code and output tail);callback(a plugin-registered handler run in this process — plugin jobs only, see Adding jobs from a plugin). - Persistent state: job dispatch state and run history live in the storage domain layer (
ctx.storage.domain, domain namecron), never in session event logs. - Reliability semantics: at-most-once per occurrence (
lastFiredMsis persisted before execution); missed occurrences are never replayed one by one (the misfire policy runs at most once, against the latest due occurrence); runs interrupted by a crash are repaired toabortedon the next startup. - Policies:
overlap: skip | queue | replace(when the previous run is still going: skip / queue the latest one occurrence / kill and restart);misfire: skip | runOnce(occurrences missed while the process was down: ignore / catch up once). - Delivery: optional delivery command; the run record is fed as JSON on stdin (fires only on failure by default).
- Clock discipline (inherited from dsh-schedule): long waits are chunked and the wall clock is re-read on every wake-up — a backwards clock jump never fires early, a forwards jump is handled as overdue.
Installation
From the Plugin Market
With dsh-market installed, open Settings → Plugin Market, search dsh-cron, and install with one click — the market adds both the dependency and the profile bundle entry for you, and most installs go live after a page refresh.
Or install the release tarball from the command line (this only runs the package install — add "dsh-cron" to dsh.profile.bundles yourself, as shown below):
dsh plugin --profile web add https://github.com/squirrel20/dsh-cron/releases/latest/download/dsh-cron.tgz
The npm package named
dsh-cronis an unrelated project — install from the market or the release tarball, not from the npm registry.
From source
In your profile's package.json:
{
"dependencies": { "dsh-cron": "link:/path/to/dsh-cron" }, // or a git checkout / release tarball
"dsh": { "profile": { "bundles": [ /* …existing bundles… */, "dsh-cron" ] } }
}
Config-declared jobs (optional)
Declare always-on jobs by overriding the config in the profile's cordis.patch.yml — or skip this entirely and create jobs from the UI or a session (see Usage):
- id: dsh-cron
config:
historyLimit: 50
jobs:
- name: daily-log-review
schedule: { cron: "0 7 * * *", timeZone: "Asia/Shanghai" }
task:
kind: agent
prompt: Read yesterday's logs under logs/, summarize anomalies and suggest remediations.
cwd: /path/to/project
timeoutSeconds: 1800
policy: { overlap: skip, misfire: skip }
delivery:
argv: ["/usr/local/bin/notify", "--stdin"]
onlyOnFailure: true
- name: heartbeat
schedule: { everySeconds: 3600 }
task: { kind: command, argv: ["./scripts/heartbeat.sh"], cwd: /path/to/project }
sessionGc: # optional; defaults: enabled: true, graceMinutes: 30, root: ~/.dsh/sessions
enabled: true
graceMinutes: 30
Job misconfiguration (duplicate names, invalid expressions, missing time zone, …) fails loud at mount time — it is never swallowed silently.
The same block takes an optional maxConcurrentRuns: 0 (the default) means unbounded — unrelated jobs have no reason to queue behind each other, and a job's own overlap is already governed by policy.overlap. Set a positive number only to deliberately cap host-wide load; 1 serializes every job, so jobs due at the same minute run one after another.
Usage
Adding a job by hand
Click + in the sidebar's Cron Jobs section header. The New job dialog configures everything on one screen:
- Name — letters (any script), digits,
-and_; no spaces. - Trigger —
cron(5-field expression + IANA time zone),interval, orone-shot. - Task —
agent(a prompt executed unattended through the full dsh toolchain) orcommand(an argv to spawn). - Preset / Access / Model — leave blank to inherit the host defaults.
- Working directory — type a path or browse via the folder icon.
- Timeout, On overlap, On misfire — see Features for the policy semantics.
Create & enable persists the job (a "manual" chip marks it apart from config-declared jobs). Afterwards, each row's ⋯ menu offers Run now / Pause schedule / Edit job / Delete job; clicking a row expands its run history; clicking an agent run opens that run's full session replay, and clicking a command run (or a pruned-session agent run) opens its run-detail page over the center column.
Adding a job from a session
Just ask the agent in any session:
Every Monday at 07:00 review our outdated dependencies and save an upgrade checklist to reports/deps-audit.md.
The bundled cron-create skill (auto-registered when the host has a skill registry) walks the model through collect → confirm → create → verify, calling the cron_create tool under the hood; cron_delete removes a manual job the same way. Jobs created from a session are ordinary manual jobs — the exact same overlay the web dialog writes — so they show up in the sidebar immediately and can be edited there later. The read/steer tools (cron_list, cron_runs, cron_run_now, cron_enable, cron_disable) work on config-declared jobs too.
Adding jobs from a plugin
A schedule often belongs with a piece of software rather than with one host: the package that ships a refresh script also knows how often it should run. Such a package can register its own jobs — installing it creates them, unmounting it retires them, and nobody transcribes a spec into a dialog on every machine.
The provider injects the cron service and registers inside an effect, exactly like a dsh-ingest source plugin:
export const name = "cron-source-kb";
export const inject = ["cron"];
export function apply(ctx, config) {
ctx.effect(() => ctx.cron.registerJobs([
{
name: "kb-refresh",
description: "Re-index the knowledge base",
schedule: { cron: "30 7 * * *", timeZone: "Asia/Shanghai" },
task: { kind: "command", argv: ["/bin/sh", `${config.repoRoot}/scripts/kb-refresh.sh`] },
policy: { overlap: "skip", misfire: "runOnce" },
},
], { owner: "dsh-cron-source-kb" }), "kb.cron()");
}
A provider that wants to run its own code — rather than shell out to a script or curl its own host — registers a callback task and passes the function:
ctx.effect(() => ctx.cron.registerJob({
name: "ingest-notes",
schedule: { cron: "10 23 * * *", timeZone: "Asia/Shanghai" },
task: { kind: "callback", timeoutSeconds: 700 },
}, {
owner: "dsh-ingest-source-notes",
run: async ({ signal }) => {
const record = await service.run("notes-research", { signal });
return { ok: record.status === "ok", summary: `${record.itemsNew} new` };
},
}), "notes.cron()");
The handler receives { job, target, seq, signal } and returns a summary string or { ok?, summary?, error? }; throwing settles the run as failed, and the timeout aborts signal. callback is the one task kind config and cron_create cannot use — a spec on disk has nobody to supply the function — so it is refused there by name.
- The spec is the same vocabulary config and
cron_createuse, and it is validated synchronously: a bad schedule throws inside the provider's ownapply, naming the field. owneris required — the overlay uses it to tell the user which package brought a job.registerJobreturns a disposer (registerJobsreturns one for the batch, and rolls back if any member is defective, so a provider never mounts half its schedules).- Registering may precede dsh-cron's own startup; registrations attach as soon as the scheduler is ready and are replayed if dsh-cron reloads.
- Names must be free: a name declared in profile config, or already registered by another provider, throws rather than silently losing to mount order.
Plugin jobs are ordinary jobs in the list — the same run history, run-now, pause and session jump-through — but they are read-only in the overlay and to cron_create / cron_delete: editing means editing the provider, and removing means uninstalling it (updateJob / cron_delete answer plugin_job). Pausing is the exception: an enable override is the user's, and it survives re-registration.
A runnable copy of the provider above lives in examples/dsh-cron-source-demo — add it to a profile's dependencies and dsh.profile.bundles to watch a plugin job appear.
Unmounting a provider stops its jobs but keeps their dispatch state and whole run history — reinstalling resumes the same job rather than starting a stranger under its name. What is left behind shows as an orphan row (marked "plugin gone", sorted last, no next occurrence) whose only action is Delete job, which clears that leftover history for good.
Run records
The runs table keeps the most recent historyLimit entries keyed by <job>#<seq>:
{
"job": "daily-log-review", "seq": 42,
"target": "2026-08-26T23:00:00.000Z", // the occurrence this run is for
"startedAt": "…", "finishedAt": "…",
"status": "ok", // ok|failed|timeout|skipped-overlap|replaced|aborted
"summary": "…", // agent's last reply / command output tail (truncated)
"sessionId": "cron-daily-log-review-…" // agent task's session, inspectable under ~/.dsh/sessions
}
Boundaries and known limitations
- Agent runs carry a fixed
[CRON RUN]framing that states the run is unattended and questions are forbidden. It is injected as a scoped system-prompt section, so the user message holds only the job's prompt; hosts without the system-prompt service fall back to prepending it to the message. - Config jobs come from plugin config (declarative); the conversational tools (
cron_list/cron_runs/cron_run_now/cron_enable/cron_disable) observe and steer them but never create or delete them. Runtime "manual" jobs are the exception:cron_create/cron_deletemanage those from a session, guided by the bundledcron-createskill (registered into the host's skill registry when one exists), through the samemanual-table overlay as the web dialog. - Plugin-registered jobs (
source: "plugin") are owned by their provider package: they can be run, paused and inspected, but not edited or deleted from the overlay or a session — that is what installing and uninstalling the provider is for. Unmounting a provider leaves an orphan row holding the job's history until the user deletes it. queuedepth is 1: only the single latest squeezed-out occurrence is kept.
Web overlay
When the profile includes @deepseek-ai/dsh-web-app, the plugin also ships a
sidebar overlay: a clock badge at the sidebar foot opens a panel listing
every job (kind, schedule, next occurrence, latest outcome); a job row
drills into its recent run history; clicking a command run (or an agent
run whose session was pruned) opens a run-detail page over the center
column — status, scheduled/start/finish instants, duration, exit code,
argv, and the stored summary tail. Rows carry hover actions — run an idle job
now (the cron_run_now semantics), or stop the run in flight (the record
settles as killed; later occurrences are untouched). The panel's +
opens a create form (name; trigger presets — hourly/daily/weekdays/weekly,
compiled to plain cron shapes and mapped back onto the presets on edit, with
cron expression/interval/one-shot in a custom tier and the time zone taken
silently from the browser; agent/command task; working directory with a
browse dialog over the host's directory capability; timeout; overlap/misfire
policy); created jobs persist in the
storage domain's manual table, re-normalize on every boot, and show a
"manual" chip beside config-declared jobs — a config job with the same name
wins and evicts the manual copy. A manual job's drill-in view carries a
two-click delete (trash, then confirm) that drops the job and its whole run
ledger; config jobs and jobs with a run in flight are refused.
The browser half is lib/client.js (declared via exports["./client"] +
the dsh.client package field). The host half (lib/web.js) serves
GET /dsh-cron/api/state plus four writes — POST …/run-now, …/stop,
…/jobs, …/delete — which demand application/json bodies so cross-site simple
requests die before dispatch; routes register on ctx.webServer only while
a webserver is present, so headless profiles mount unchanged.
Tests
npm test # unit tests for the scheduling math (cron parsing, time zones, anchor alignment, misfire collapsing)


GanyuanRan/Aegis
shengsheng90/DSH-taskboard
pengyue-polaron/deepseek-harness-genui
Jayden-X-L/forkprobe
BeforeWave/dsh-with-chatgpt
sluminositys/dsh-nested-followups
Meredith2328/dsh-sticky-note
cloader/dsh-taskboard