fuhefei/dsh-sentinel
Condition-driven wakeup for DeepSeek Harness: durable file/command/http/process/webhook watches that wake the agent, with dock, sidebar branch, and a global dashboard.
已收录
11
Workflow
Bundle 已验证
预览
功能介绍
条件驱动唤醒:file/command/http/process/webhook 持久监视,触发即唤醒 agent。
适合
- 需要在文件、命令、HTTP 端点、进程、端口或 webhook 条件变化时恢复工作的 Agent。
- 需要监视任务跨 Session 关闭和进程重启持久存在的长期运行 DSH Web 部署。
- 希望查看 Session 级监视状态、全局仪表盘,并可选发送外部触发通知的运维人员。
不适合
- 期望一次性 DSH 进程退出后仍继续探测的工作流;探测需要常驻进程。
- 要求严格恰好一次投递的工作流;崩溃恢复后的唤醒采用至少一次投递。
- 需要超过当前配置的单 Session 订阅数或待处理唤醒数上限、且无法调整配置的场景。
README
dsh-sentinel
| English | 中文 |
Condition-driven wakeup for DeepSeek Harness: the agent registers a watch, goes to sleep — even closes the session — and the sentinel wakes it when the condition happens. Every subscription and every fire is a user-visible session event, and the browser dock shows what is on duty.

How it works
The node half owns one server-lifetime runtime that folds a plugin-owned sidecar log ($DSH_HOME/sentinel.jsonl) into live subscriptions, probes every sensor on a shared 5s heartbeat, and delivers wakeups through the official followup channel — resuming a dormant session’s agent first when needed. Subscriptions therefore survive process restarts, and conditions that become true while the server is down late-fire on the next probe.
Watching is a resident-process concern: probing and fire delivery only run while a long-running dsh process (typically dsh web) is up. Headless one-shot runs load the plugin and can create, list and cancel watches, but nothing probes after the process exits — those watches become active once a resident process starts.
One duty owner per $DSH_HOME: a lease file (sentinel.lease) makes the first process own probing and delivery; a second dsh process on the same home stays passive (tools work, writes persist to the shared sidecar) and takes over within one lease TTL of the owner dying. The owner re-reads the sidecar every heartbeat, so watches created on a passive instance are adopted automatically. Delivery is at-least-once: a fire logged but not delivered before a crash is requeued on the next boot from its delivered watermark.
The browser half is a dock card above the composer (the conversation.input.dock family) listing the session’s active watches — sensor, target, live probe state, fire budget, next-probe countdown — plus recent fire history when expanded. It polls the read-only state route and renders nothing when the session has no watches.
Two surfaces make the server-global watch set visible. A sidebar branch grows under every session row that has active watches (sidebar.workspaces.sessionRow.branch, one shared poller for all rows) — collapsed it is a 👁 count, expanded it lists the session’s watches and links to the dashboard. The dashboard is a standalone table of every watch across every session: session (active/dormant), sensor, target, pattern, fire budget, last probe state, next probe.
| Sidebar branch | Global dashboard |
|---|---|
![]() |
![]() |
Sensors
| Kind | Engine | Fires on |
|---|---|---|
file |
path snapshot + inotify push | snapshot change (sub-second); accelerated by fs events |
command |
read-only shell line, probed on an interval | output/exit-code change |
http |
URL probed on an interval | status/body change |
process |
pgrep -f pattern, probed on an interval |
match-set change |
port |
TCP connect to [host:]port, probed on an interval |
reachability change (open/closed/timeout) |
webhook |
pure push | any POST to the returned hook URL |
With pattern, probe kinds fire on the no-match→match edge of that regex and webhooks accept only matching payloads; without it, probe kinds fire on any change after the baseline.
Configuration
All deployment-tunable knobs live in the plugin’s config schema (defaults in parentheses); override them on the bundle row in your profile’s cordis.patch.yml:
- id: dsh-sentinel
name: dsh-sentinel
config:
heartbeatMs: 5000 # probe round interval
probeConcurrency: 8 # in-flight probes per round
maxSubscriptionsPerSession: 16
maxPendingWakeups: 8 # queued wakeups per session before dropping oldest
defaultIntervalSeconds: 30 # when a watch does not specify one (5–86400)
defaultCooldownSeconds: 60
dutyLeaseTtlMs: 30000 # passive-instance takeover window after the owner dies
notifyWebhookUrl: '' # optional: POST every fire here as JSON
Invalid values fail plugin load with a schema error rather than misbehaving at runtime.
notifyWebhookUrl fans every fire out of the harness as a JSON POST ({plugin, event, sessionId, id, kind, target, note, fireNumber, maxFires, summary, after}) — point it at a Lark/WeCom/Slack bot or any receiver. Delivery is at-most-once: a failed POST warns in the log and never blocks the in-harness wakeup.
Tools
-
sentinel_watch— register a watch:kind,target, optionalpattern,interval(1–3600s, default 30),note(delivered verbatim with every wakeup),maxFires(default 1: one-shot),cooldown(default 60s), optionalttl. -
sentinel_list— active watches with live probe state. -
sentinel_cancel— cancel one watch by id.
Routes
-
GET /plugins/dsh-sentinel/state?sessionId=…— read-only state for the dock and the sidebar branch (omitsessionIdfor every session). -
GET /plugins/dsh-sentinel/dashboard— the server-global watch table. -
POST /plugins/dsh-sentinel/hook?id=watch-N&s=<sessionId>— webhook entry; put acurlinto a CI job, git hook, or another machine’s script to wake the agent. Watch ids are per session, so thesqualifier is what keeps two sessions’watch-1hooks from colliding (the tool hands out the full URL). URLs withoutsstill work and resolve to the first matching webhook watch. -
POST /plugins/dsh-sentinel/cancel?sessionId=…&id=watch-N— manual cancel. The dashboard table and every UI row carry a ✕ that calls this, so a watch can always be stopped by hand — including orphaned ones whose session (and agent) is long gone; the host has no session-deleted event, so this is the kill switch of last resort. - All four routes enforce a browser-trust fence: browser-marked cross-site requests (a malicious page can form-POST to localhost) and DNS-rebinding attempts (Host/Origin naming a DNS host) get 403. Headerless clients such as
curland CI jobs are unaffected. The state route also reportsduty(lease heartbeat age) anddroppedWakeupsper session (queued wakeups dropped by themaxPendingWakeupscap).
First-probe semantics: a pattern-less watch absorbs its first observation as the baseline (no fire), while a pattern watch whose target already matches fires on the first probe — the condition already holds.
Compatibility
Verified against these harness versions (plugin loads, duty lease is held, web routes answer):
-
0.1.0-rc.8— 2026-08-20, scratch-profile smoke -
0.1.0-rc.7— 2026-08-20, live web deployment
dsh-sentinel has no runtime dependency on @deepseek-ai/* packages. Compatibility here means the cordis loader entries, the ctx.agents.resume() followup channel, and the web routes keep working; file an issue if a harness version breaks any of them.
Install
One line through the official bundle channel:
dsh plugin --profile web add dsh-sentinel
Or straight from git (build artifacts are committed, so the git-source install runs no build):
dsh plugin --profile web add "github:fuhefei/dsh-sentinel#v0.11.0"
Alternatively, add the node half manually through a patch-list configuration over the shipped base:
# cordis.patch.yml
- insert:
- id: dsh-sentinel
name: dsh-sentinel
The browser half ships in the same package (./client) and is injected by the Web UI’s plugin loader.
Sidebar branch prerequisite
The dock and the dashboard work on a stock host. The sidebar branch needs the session-row extension holes, which the official tree does not declare yet; apply the bundled patch to your DSH source checkout and rebuild ui-workspace:
git apply /path/to/dsh-sentinel/patches/session-row-holes.patch
The patch declares sidebar.workspaces.sessionRow and sidebar.workspaces.sessionRow.branch as list holes (every registrant renders, in order) at root scope (sidebar rows render outside any session binding; the row passes its sessionId through owner props). dsh-subagent-tree ships a patch for the same hole names with different semantics (keyed/session); apply one or the other, not both.
better-sidebar integration (optional)
When dsh-better-sidebar is installed in the same profile, sentinel registers its global watch table as a sidebar tab (dsh-sentinel:watches, in the + menu) through better-sidebar’s documented ctx.betterSidebar.registerTab extension surface: every watch server-wide with live probe state, fire budgets and recent fire history, fed by one shared poller. No configuration needed; without better-sidebar the registration is silently skipped and the dock / branch / dashboard keep working as before.

Plays well with
Install dsh-notification alongside sentinel and the whole wakeup loop reaches your desktop: sentinel wakes the agent, the agent works the turn, and the turn’s completion fires a desktop notification — no integration needed, the two plugins compose on their own.
Develop
npm install
npm run build # tsc -b + tsdown (lib/index.js, lib/client.js)
npm test # vitest: domain fold/normalize, sensors, dashboard escaping, e2e wakeup flow
License
BSD 3-Clause. See LICENSE.
常见问题常见问题
在启用了 DSH 的终端中执行已验证命令 dsh plugin --profile default add github:fuhefei/dsh-sentinel。命令会解析公开 package 元数据,并保持插件与本页展示的目录身份一致。
兼容性以页面上展示的 bundle 与 profile 状态为准。如果某个 profile 尚未检测到,请先保持禁用,并在生产启用前阅读仓库文档。
GitHub 链接和 activity 元数据是 release 与维护状态的来源。新版本发布后重新查看本页,确认目录已经观察到最新版本。

