返回目录
其他 待识别

dsh-cron

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
Forks
0
Issues
0
更新
21 天前

PROJECT TOPICS

项目标签

PROJECT README

README

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:

When Which link broke What they had in common
Day 1–2 one letter wrong in a path inside the script silent
Day 3–4 credential expired; the session opened, the work never happened silent
Day 5 never fired, no reason recoverable from system logs silent

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

Tool What it does
cron_add Schedule a task (with a check, or it is not really scheduled)
cron_list Current tasks and their window state
cron_status Read the append-only run ledger — the answer to "did it run today?", as evidence, not a summary
cron_remove Remove / pause / resume
cron_run_now Run 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").

The wire is not a constant

fire.kind: "session" opens a real session over the host's public wire. That wire was rebuilt in 0.1.2-alpha.1: dotted endpoints without auth (POST /api/session.prompt) became slash endpoints with a cookie, a double-wrapped payload, and a client-minted requestId. So this package does not read a version number — it probes each host and speaks whatever that host speaks, then records which one in the ledger.

That indirection exists because of a trap worth stating on its own:

After an in-place source upgrade, an already-running host keeps executing the old code from memory, while the directory name, package.json and git describe all report the new version.

Measured on one machine, same day: a host started before the upgrade answered unauthenticated agentPreset.list with ok; a host started after it answered 401 to the identical call. Nothing on disk distinguishes them — only their replies do. Compare ps -o lstart against the checkout's upgrade time; do not trust the directory name.

Two more facts the client encodes, both measured:

  • The double-wrap key is per endpoint. session/create and session/prompt take {args:{request:{…}}}; session/list takes {args:{_request:{…}}}. The gateway's gateway/arguments-invalid error names the missing and unexpected fields verbatim, so the client reads the error rather than guessing.
  • The auth cookie outlives the host. The launch token is per-process, printed once to stdout, never persisted — but the cookie it buys is signed with a stored secret and bound to host:port, so it keeps working across restarts for 30 days. Exchange once; unattended work then survives every restart.
  • Mid-session questions no longer need a human. An unattended session stalls the moment the agent calls ask-user-question. The event plane is one WebSocket carrier (/api/remote.mux, behind the same cookie gate; Node ≥ 22's built-in WebSocket takes a non-standard {headers} init, so this stays dependency-free): open a $events stream — the gateway insists on a literal empty {args:{}} and says so verbatim if you send anything else — read ready.clientId, then answer any user-questions/request waterfall frame whose agentId equals your session id via POST /api/$events/result. Measured end to end on 0.1.2-alpha.2: the agent asked, attendQuestions() picked a label, the agent resumed with the chosen answer. node lib/cli.js attend <port> <sessionId> does this from a shell script.

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

CLASSIFICATION EVIDENCE

分类依据

项目类型待识别
功能分类其他
规则置信度

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。