deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
English | 中文
Scheduled work for DeepSeek Harness that survives session end, host restart, and machine sleep — because it schedules an outcome, not a moment.
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.
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:
cron skips a sleep-missed run and never reruns it.)| 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.
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.
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").
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.jsonandgit describeall 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:
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.host:port, so it keeps working across restarts for 30 days. Exchange once; unattended work then survives every restart./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.test -f on a file that is always there) will cheerfully report that everything is fine.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,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。