<table> <tr> <td width="40%" valign="top">
dsh-watch
English | 中文
给数据流挂一个监听。它说话,agent 就醒 —— 哪怕正在空闲,哪怕现场没有人。
自带的 jobs 子系统只说任务结束了。dsh-watch 说有东西开口了: 给长跑命令或持续增长的文件挂一个监听器,新出现的行经过滤、合批、字节封顶后, 作为会话内通知送达。
然后把人撤掉。把监听写进 profile 配置、挂上 daemon,agent 就会开机自启、 空闲时零成本静默、连续数周被数据流唤醒。
   
</td> <td width="60%" valign="top">
<img src="https://raw.githubusercontent.com/dshworks/dsh-watch/main/docs/watch-dark.png" alt="终端里运行 dsh --profile watcher:daemon 启动、报告 Standing by、随后静默空闲不花一分钱,接着一次听到 53 个新发布的仓库,并记下留用的 11 个及各自归属的 dshworks 仓库" width="100%">
</td> </tr> </table>
安装
dsh plugin --profile web add @dshworks/dsh-watch
dsh --profile webdsh plugin 会转发给 pnpm,所以 PATH 里要有 pnpm。除此之外不需要配置 —— 下一个会话里 watch 工具就可用了。
工具本体
watch(source: "command", command: "npm run dev", pattern: "error|warn|Ready", label: "dev")
→ Watch armed (watch-1) on command: npm run dev.
# …agent 继续干自己的活。当 dev server 打出 "Ready in 130ms",
# 一条通知把它唤醒:
[watch dev · watch-1] 1 line:
Ready in 130ms也可以盯文件:
watch(source: "file", path: "/var/log/app.log", pattern: "ERROR|FATAL")每个监听都是一等公民后台任务(kind 为 watch),因此标准接口原样适用: job_list 列出已布防的监听,job_output 取出某个监听积压的行,job_kill 撤防, 归属按会话隔离,结束时走普通的完成通知。这个插件只增加"听"这件事 —— 没有第二套生命周期,也没有第二个注册表。
| 行为 | 细节 |
|---|---|
| 数据源 | command(经 shell 能力启动一次,继承会话的沙箱策略与环境变量)· file(从当前末尾开始跟踪;已有内容一律不投递;被截断则从头重来;文件还不存在也可以先等着) |
| 合批 | 一个 tick 内听到的所有行共用一条通知 —— 爆发式输出只花一次唤醒,而不是每行一次 |
| 过滤 | 可选的 JavaScript 正则,只投递命中的行。失败特征也要写进去 —— 没声音不等于没问题 |
| 唤醒预算 | 每个 owner 一个令牌桶:先允许 maxConsecutiveWakes 次连发,之后每 wakeRefillMs 回一个额度。用户消息被领取时直接加满 |
| 事件预算 | 每个监听在 max_events 条通知后自行撤防并以 completed 结束;填 0 表示一直听下去 |
| 字节上限 | 每条完整通知按 UTF-8 安全截断(含包装文本);job_output 保留有上限的积压,丢行时带明确的裁剪标记 |
| 截断诚实 | 上游输出丢失(lossy 读)会作为标记行暴露出来,绝不吞掉 |
| 生命周期 | 进程退出时先冲刷尾部再结算:completed(退出码 0)、failed(非零 —— 监听死了是一个发现,不是一段沉默)、killed(信号或撤防)。插件卸载会拆掉所有监听 |
| 上限 | 每个 owner 的布防数量上限,超了就让这次调用明确失败 |
无人值守运行
一个连续盯一个月生态的 agent,不是一场对话。有两件事必须改,而两件都已在包里。
它不能变聋。 唤醒预算的存在,是为了不让一个失控的数据源把空闲 agent 无限循环唤醒。 "数连续唤醒次数、等下一条人类消息回血"这条规则,对有人坐在旁边的会话是对的 —— 对没人的会话就是一扇单向门:额度用完之后,每条通知都被注入到一个再也不会被唤醒的 空闲 agent 里。所以预算改成了令牌桶:额度随时间回来(wakeRefillMs,默认 60 秒), 而在桶空时到达的通知,会在额度回来的那一刻拿到一次补课唤醒。想要严格的 dsh-tool-jobs 语义,把 wakeRefillMs 设为 0。
得有人去布防,也得有人让进程活着。 dsh --profile headless 跑完一个任务到静默 就退出;dsh --profile web 常驻,但要等浏览器里的人。于是 autoArm 把监听声明进 profile 配置 —— 在 agent/session-start 时为根会话布防,并且走工具注册表, 因此它要过的守卫、审批策略、沙箱和 shell 环境,跟模型自己发起的调用完全一致 —— 而 @dshworks/dsh-watch/daemon 是一个约 90 行的宿主:创建一个 agent、注入一份常驻 简报、把进程撑住,并把每个活动区间的收尾文本按 ISO 时间戳写到 stdout 作为运维日志。
# ~/.dsh/profiles/watcher/cordis.patch.yml
- id: dsh-watch
config:
wakeRefillMs: 300000
autoArm:
- source: command
command: node recipes/ecosystem-watcher/feed.mjs --topic dsh-plugin
pattern: '"kind":"new-repo"'
label: ecosystem
max_events: 0 # 会自己撤防的监听,算不上监听
- insert:
- id: dsh-watch-daemon
name: '@dshworks/dsh-watch/daemon'
config:
brief: >-
You are the ecosystem watcher. A standing watch named "ecosystem"
is armed on a feed of newly published repositories. For each one,
decide whether there is an idea worth keeping, and append it to
data/ideas.ndjson.dsh plugin --profile watcher add @dshworks/dsh-watch
dsh --profile watcher | tee -a watcher.log[recipes/ecosystem-watcher/](recipes/ecosystem-watcher/) 里是完整的一套, 包含一个按创建时间轮询 GitHub topic、对每个没见过的仓库输出一行 NDJSON 的 feed。
实测
2026-08-15 的一次真实运行,DeepSeek-V4-Pro,会话里没有人。feed 找到 53 个新发布的 dsh 仓库;watcher 被唤醒、读完、留下 11 个,并说明了另外 42 个为什么被放掉:
2026-08-15T16:21:23Z up — session 36041f74
2026-08-15T16:25:11Z Batch of 53 repos processed. Appended 11 ideas to data/ideas.ndjson{"repo":"liustack/modlens","why":"Pasting an image and receiving structured JSON evidence
(OCR, layout, semantics) gives text-only DSH models vision without a vision model.",
"for":"awesome-dsh-plugins"}这次运行还顺带换来一个 bug 修复:注入给忙碌 owner 的通知会在它的下一步被领取, 所以把它们算作"欠一次补课唤醒",就会多出一次唤醒,而那次唤醒的全部内容是 "你有排队的通知" —— 说的却是已经回答过的通知。改成在 agent/inbox/claimed 时清零, 并补了一个回归测试守住。
配置
每一项边界都是经过校验的 Config 字段,不是写死的常量。
| 字段 | 默认值 | 含义 |
|---|---|---|
pollIntervalMs | 300 | 轮询节奏,同时也是合批窗口 |
maxNoticeBytes | 4096 | 单条完整通知(含头部)的 UTF-8 字节上限 |
maxConsecutiveWakes | 3 | 每个 owner 的唤醒额度桶容量 |
wakeRefillMs | 60000 | 回一个额度所需的毫秒数;0 表示不按时间回血 |
defaultMaxEvents | 50 | 每个监听的通知预算;0 表示不限 |
maxListenersPerOwner | 8 | 每个 agent 的布防数量上限 |
backlogBytes | 65536 | 单个监听 job_output 积压的保留字节预算 |
autoArm | [] | 启动时为根会话布防的常驻监听 |
daemon 自己的配置是 brief(必填)、flushIntervalMs(300000)和 journal(true)。
Model Experience
系统提示词
一段(order 107,排在后台任务指引之后):
You can arm a watch on a stream with the watch tool: it listens in the background and wakes you when new matching lines arrive, so never busy-poll a source you already listen to. Listeners are background jobs — job_list shows them, job_output reads a listener's heard-line backlog, job_kill disarms one.配置了 autoArm 时,会追加第二段(order 108)列出常驻监听的名字, 免得模型把已经有的东西再布防一遍。
工具
只有一个工具 watch(source、command/path、workdir、pattern、max_events、label)。 撤防、列表、读取都复用自带的 job_kill / job_list / job_output。
Token 影响
安静的数据源不花 token —— 轮询发生在宿主侧。每条通知是一条有字节上限的消息 (≤ maxNoticeBytes);空闲唤醒还要额外付它开启的那次模型请求, 这正是唤醒需要预算的原因。
依赖要求
- jobs 子系统:
@deepseek-ai/dsh-jobs、dsh-jobs-local、dsh-tool-jobs——
都在 dsh-base 里,因此 web 与 headless 标准 profile 都自带。 没有挂上 job 控制器时,布防会明确失败。
source: command还需要 shell 能力(@deepseek-ai/dsh-shell加一个 provider)。
文件源不需要。
- daemon 另外需要
agents、sessions、agentDefaultModel—— 同样在dsh-base。
开发
pnpm install && pnpm test # 83 个测试lib/ 里是纯 ESM JavaScript —— 安装时不构建任何东西,所以用 git 装不会碰到 allowBuilds。(开发安装会:vitest 会拉 esbuild,在 pnpm-workspace.yaml 里放行 —— pnpm ≥ 11 从那里读构建设置,package.json 里的 pnpm 字段会被静默忽略。)
除了测试套件,完整生命周期都在 0.1.0-rc.6 + DeepSeek-V4-Pro 的真实会话里验证过: 布防 → 不匹配的行保持沉默 → 空闲时命中即唤醒 → 用自带 job_kill 撤防 → 撤防后彻底安静(2026-08-14);以及启动 → 常驻简报 → 空闲 → 被无人值守的 feed 唤醒两次,从写入到回复约 1.6 秒(2026-08-15)。
与邻居的关系
- 自带的
dsh-tool-jobs在任务完成时通知;dsh-watch 在运行期间有输出时通知。
同一套唤醒机制,互补的两个时刻。
通过分流输出来订阅已经在跑的 bash/pwsh 任务。想盯一个你已经启动的任务用它; 想布一个带合批和预算的专门监听(或盯文件)用 dsh-watch。
反复重跑一个命令、或轮询 NDJSON 收件箱,逐行唤醒 agent。dsh-watch 的不同在于: 命令只启动一次并按流处理、按 tick 合批、给唤醒和事件都上预算、每条通知都有字节上限、 活在 jobs 子系统内部,以及自带一个无人值守宿主。
已知限制与未做的事
- 是轮询,不是
inotify/kqueue:低于pollIntervalMs的延迟不在范围内,
tick 本身就是设计中的合批窗口。
- 每个监听只有一个正则;多种特征用或来覆盖(
error|Traceback|FAILED)。 - 命令源的
stderr混在 shell provider 的标记段落里,不是一条单独可过滤的通道。 - 重启后不会自动恢复:监听与它的会话同生共死。重启后的 daemon 会开一个新会话并按
autoArm 重新布防,而不是接着上一个会话。
- 补课扫描挂在监听的 tick 上,所以一个 owner 如果所有监听都被撤防了,
排队的通知会一直留到有别的东西唤醒它为止。
许可
MIT。与 DeepSeek 无隶属关系。已登记在 awesome-dsh-plugins。