Memory
dsh-timer-task
0gl20shk0sbt36/dsh-deadman · ★ 0
DSH deadman switch: run a command unless a living agent keeps holding it off. Arms a watchdog that asks the owning session on every firing, skips when somebody holds, and acts when nobody answers.
安装
dsh plugin --profile web add 'github:0gl20shk0sbt36/dsh-deadman#8f642fea9e155757a08eaa9fd261b6eeeadaba49'GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
English | 中文
A deadman switch for DeepSeek Harness (dsh): once armed, it periodically checks whether any work is still advancing, and runs your command when nobody holds it off.
Built for unattended long runs — overnight builds, training, batch rendering, cloud batch jobs. A plain "shut down at 03:00" kills work that is still running; a deadman switch asks the opposite question: is anybody still making progress? If nobody is, reclaim the resource.
How it differs from the built-in scheduled tasks
built-in dsh-schedule |
dsh-deadman | |
|---|---|---|
| What happens when due | a message is enqueued into the session inbox and the model decides what to do | the configured shell command runs directly, no model involved |
| Blocking mechanism | none | hold gate: while any agent keeps advancing, execution is postponed; it runs only when nobody holds |
| Fits | calendar reminders, waking a session | unattended stop-loss: shut the machine or instance down once the work is done or the agent is gone |
They complement each other. The built-in scheduler assumes the model is alive to handle the message; the moment a resource should be reclaimed is often exactly the moment the model has died or run out of quota.
Install
Requires dsh 0.2.0-rc.2 (peerDependencies are pinned exactly; dsh refuses to install plugins whose peers do not match, and says so).
# from a local path
dsh plugin --profile web add /path/to/dsh-deadman
# or from GitHub
dsh plugin --profile web add github:OWNER/dsh-deadman
Web / Desktop: enable dsh-deadman on the Plugins page. CLI: dsh plugin --profile <name> remove dsh-deadman to uninstall.
Install it into a long-running profile (such as
web); see Known limitations. Dependencies are pinned to 0.2.0-rc.2. Installing from GitHub lets pnpm install them fromdependencies; installing from a local path uses pnpm'slink:protocol, which does not resolve dependencies — runpnpm installinside the plugin directory first so its ownnode_modulesexists.
Model-facing tools
| Tool | Purpose |
|---|---|
deadman_arm |
Arm: name / command / after_minutes / interval_minutes / description. First inspection after after_minutes; a held round re-inspects after interval_minutes; one successful execution ends the task, nothing has to be cancelled afterwards |
deadman_hold |
Report "still working": skip the current round and re-arm. Any session may hold (fail-safe semantics) |
deadman_disarm |
Disarm. Only the arming session (or one of its direct subagents) may; other sessions need an explicit force=true, which is logged |
Task names are globally unique within the process; arming an existing name is rejected and names the session that owns it.
Slash command: /deadman (this session) · /deadman all (every session) · /deadman cancel <name>.
Decision order of one firing
- Resolve the owning session: try
ctx.agents.get(); if the session is not open,ctx.agents.resume()brings it back so delivery cannot fall into a black hole. - Deliver the inspection notice with
agent.send(message, "next-step", true)— a running agent sees it at its next step boundary, an idle one is woken. The notice explicitly forbids asking the user or requesting confirmation: hold, or stay silent. - Inspection loop, keyed on progress rather than status:
- any agent produced new session events since the last check (
session.seqadvanced) ⇒ still advancing, keep waiting; - nothing advanced ⇒ start the grace window
holdWindowSeconds; - a pending user question exists (
state === 'open'in theuserQuestionsprojection) ⇒ timed separately, released after at mostquestionBlockSeconds; - total ceiling
maxHoldMinutessince the firing — whatever happens, it runs when the ceiling is reached (fail-open).
- any agent produced new session events since the last check (
- Released ⇒ run the command; on success the task finishes by itself and leaves a notice for the owner; if it was held ⇒ re-arm for the next interval.
Hold channels
- the
deadman_holdtool (the lead agent, or a subagent granted it); - a marker file
.deadman-hold-<name>in the current working directory (useful when a subagent's tool allowlist has nodeadman_hold); - a file named after the task inside the plugin's
hold/directory (external scripts, manual intervention).
Behaviour with several sessions
- Every task remembers its owning session and asks only that session; while the owner is running it also notifies other running agents (the ones actually doing the work).
- The progress criterion is global by default (
progressScope: global): as long as anything anywhere still advances, every switch waits — the correct semantics for "shut the machine down". SetprogressScope: ownerto watch only the owner and its direct subagents. - Switches that come due together run their commands concurrently (there is no global lock); enable
serializeCommandsto queue them instead. - Holding is unrestricted (anyone may call work off); disarming is restricted (see
deadman_disarm).
Configuration
Override the bundle defaults in the profile's cordis.patch.yml:
- id: deadman
config:
holdWindowSeconds: 60
| Key | Default | Meaning |
|---|---|---|
holdWindowSeconds |
60 |
how long to wait after every agent stopped advancing before executing |
checkIntervalSeconds |
20 |
inspection period |
maxHoldMinutes |
30 |
total ceiling counted from the firing; released unconditionally when reached |
questionBlockSeconds |
600 |
longest wait while blocked by a pending user question, then released |
onUndeliverable |
execute |
when the owning session cannot be delivered to at all: execute anyway / skip this round |
progressScope |
global |
progress criterion: global (whole harness) or owner (owner plus its direct subagents) |
serializeCommands |
false |
queue action commands instead of running them concurrently |
commandTimeoutSeconds |
300 |
execution timeout of a single action command |
dataDir |
$DSH_HOME/plugins/dsh-deadman |
data directory (tasks.json + hold/) |
Data and persistence
tasks.json: the task table, loaded at plugin start and durable across restarts (an overdue task is handled on the next inspection).hold/: external hold markers.- Fields:
name/command/description/dueAt/intervalMin/sessionId/createdAt/firedCount.
Known limitations
- Only effective in a long-running instance: the scheduler lives inside the dsh process. A switch armed in a one-shot headless process has nobody to inspect it after that process exits (unless a later long-running instance loads it at boot).
- Single-writer assumption:
tasks.jsonis a plain JSON file; concurrent writers overwrite each other. Do not arm from several processes at once — the plugin is meant to be installed only in the long-running profile. progressScope: ownercounts the owner and its direct subagents only; deeper descendants are not counted.- The question gate depends on the
userQuestionssession projection; without that projection the gate is simply inactive (the other gates still apply, nothing is misjudged). - Exactly-once execution is not guaranteed: a host crash can leave an executed action unrecorded.
- The three gates (both
progressScopevalues, the question-block release, and themaxHoldMinutesceiling) are covered by the fake-host suite innpm test(tests/fake-host.mjs, deterministic). Not covered: the real 600-second question timeout inside a live session, and arming from several processes.
Safety
- The command is entirely caller-supplied: only schedule commands you trust.
- The default semantics are fail-open (nobody holds ⇒ it runs). That is the point of a deadman switch, but choose sensible intervals and grace windows for destructive actions.
- The plugin makes no network calls and collects nothing; it only writes inside its own data directory.
Development
pnpm install # dependencies are pinned to 0.2.0-rc.2
node --check lib/index.js
Restart the dsh process after changing the code: ESM modules are loaded at startup, and hot reload only covers the profile configuration layer.
License
MIT