Back to home@TT-Wang

dsh-cron

Scheduled work for DeepSeek Harness that survives session end, host restart and machine sleep — it schedules an outcome (completion window + effect check + catch-up), not a moment.

Stars
1
Language
TypeScript
Created
Aug 26, 2026
Updated
Aug 26, 2026

Introduction

dsh-cron

English | 中文

Scheduled work for DeepSeek Harness that survives session end, host restart, and machine sleep — because it schedules an outcome, not a moment.

Why this exists

DSH ships a schedule family, but it is deliberately an in-session reminder. Its delivery boundary is written into the type:

/** Fixed v1 delivery boundary: the original session must be live. */
type ScheduleDeliveryMode = 'session-local'

with the note that "the original Session must be live: no external notification channel or cold-session scheduler exists", and a package README that says it "intentionally exposes no Schedule service or mutable database".

That is a scope decision, not a defect. In DSH the unit that does work is a session with a live agent, and nothing outside a live session is allowed to hold state that drives work. A cron needs a process that outlives every session — a different thing. This plugin is that thing: it lives in the host process, survives across sessions, and calls sessions up from the outside when work is due.

What makes it different from crontab / launchd

It schedules by outcome, not by clock time.

Triggers were never the scarce part. Here is a real failure log from one small daily pipeline:

WhenWhich link brokeWhat they had in common
Day 1–2one letter wrong in a path inside the scriptsilent
Day 3–4credential expired; the session opened, the work never happenedsilent
Day 5never fired, no reason recoverable from system logssilent

Three breaks, three different links, and not one of them was the trigger. Adding a more accurate trigger just buys a fourth silent failure mode.

So the unit here is a completion window:

cron time ──────────── completion window ──────────→ window closes
   │                                                       │
   ├─ ask the check: did the thing actually happen?        │
   │     yes → done                                        │
   │     no  → fire once, ask again after retryEvery ──────┤
   │                                                       ↓
   └──────────────────────────────────────→ still not done = MISS, recorded loudly

The guarantee is "it gets done today", not "it fires at 09:00". A machine that sleeps and a host that restarts cannot honour the second one, and promising it produces exactly the silent misses above.

Two properties fall out for free:

  • Catch-up. A window missed while the host was down is picked up on the first tick after it comes back. (Plain cron skips a sleep-missed run and never reruns it.)
  • Someone else's work counts. If a human already did the thing by hand, the check says so and nothing fires. Scheduling by outcome means you want the thing to have happened, not to have fired.

Tools

ToolWhat it does
cron_addSchedule a task (with a check, or it is not really scheduled)
cron_listCurrent tasks and their window state
cron_statusRead the append-only run ledger — the answer to "did it run today?", as evidence, not a summary
cron_removeRemove / pause / resume
cron_run_nowRun one evaluation pass immediately, to test a task without waiting for its window

Example — a daily report that must go out within six hours of 09:00:

{
  "id": "daily-digest",
  "cron": "0 9 * * *",
  "tz": "Asia/Shanghai",
  "window": "6h",
  "retryEvery": "30m",
  "check": { "kind": "command",
             "run": ["/bin/sh", "-c", "grep -q \"$(date +%F)\" ~/reports/sent.log"] },
  "fire":  { "kind": "session", "preset": "digest-bot",
             "prompt": "Produce today's report and send it", "cwd": "/home/you/reports" }
}

fire is either session (opens a real unattended session, prefixed with a discipline header: nobody is there to answer questions, a failed attempt means try another way, finish the job) or command (deterministic work needs no model).

check is either command (exit 0 means done) or http (2xx, optional contains). No model is involved in deciding whether the work happened — a session reporting "sent!" is the least trustworthy evidence on the chain.

What happens without a check

It degrades to plain cron: one blind shot per window, and nobody ever learns whether it worked. The cron_add receipt, every cron_list row, and every ledger line say so explicitly. Degrading is allowed; degrading silently is not.

Install

npm install && npm run link:dsh && npm run build

Add the dependency to your DSH profile's package.json, then insert the plugin in cordis.patch.yml:

- insert:
    - id: dsh-cron
      name: '@dsh-external/dsh-cron'

Config (all optional): root (data directory, default $DSH_HOME/cron), tickMs (evaluation interval, default 60s), wirePort (host port used to open sessions; read from the host's web server by default).

State lives in $DSH_HOME/cron/: tasks.json (definitions and window state, written atomically) and runs.jsonl (append-only ledger, never rewritten — a state file that gets overwritten cannot answer "what actually happened today").

Honest limits

  • The plugin lives inside the host process. If the host is down, so is this. "Who wakes the waker" moves up one level rather than disappearing; starting the host at boot is still launchd's job.
  • The quality of this package equals the quality of your checks. A loose check (say, test -f on a file that is always there) will cheerfully report that everything is fine.
  • The ledger is append-only and does not rotate itself.
  • Single machine, single host. No cross-machine coordination, no distributed lock.

Development

npm test     # builds, then runs the suite

The decision core (src/schedule.ts) is pure — no clock, no disk, no processes — and every one of its tests is written against a real-world break: host down through the cron time, machine waking up, a credential expiring mid-window, a human doing the work by hand, a window closing unfinished.

MIT