dsh-session-wake
Manifest validInstant wake for running DSH sessions — a host-plane bridge using the official sessionController.prompt() API (HTTP + file queue). Why headless can't.
dsh-session-wake
从外部进程即时唤醒一个正在运行/已持久化的 DSH 会话,让它在几毫秒内接受一轮新 turn。 支持
standard、ptc、minimal等任意 agent preset,冷会话/忙碌中的会话都行——这是dsh --profile headless做不到的。
外部进程 ──HTTP POST /wake──┐
└──写文件队列───────┤ dsh web 进程内(host 插件)
└─▶ ctx.sessionController.prompt() ──▶ 目标会话新 turn(GUI 里直接可见)
- ✅ 不重启 DSH、不换 preset、不动会话历史
- ✅ 忙时排队不打断(
mode:"queue")、冷会话自动 resume - ✅ 两条触发通道:HTTP(毫秒级)与文件队列(dsh 挂了请求积压、起来补投)
- ✅ 零运行时依赖(只用
node:内置模块),单文件lib/index.js
为什么需要它(以及为什么 headless 不行)
DSH 官方唯一「脚本唤醒会话」入口是 headless:
dsh --profile headless --session-id <id> "继续干活"
但它只能驱动「从未记录过 agent preset」的裸 core 会话。只要会话记录过 preset(Web GUI 创建的会话都记录),就报:
dsh: session "session-..." runs under agent preset "standard",
which the one-shot runner does not compose
根因不是 profile 组合不全,而是 @deepseek-ai/dsh-headless/lib/index.js 里的硬判断:
function assertAdoptable(header, events, sessionId, cwd) {
const preset = currentPreset(header, events, sessionId);
if (preset !== void 0) throw new Error(`... does not compose`); // ← 无条件拒绝
...
}
补 bundle、写 --patch 都不会改变这个判断(0.2.0-rc.2 读源码确认)。
正确入口是浏览器客户端发消息所用的同一个官方 Host 服务 —— ctx.sessionController.prompt():
Admit one prompt after explicitly resuming its Session.
它按会话记录的 preset resume/组合 Agent,所以冷会话、standard 会话都能被它拉起来。本插件就是把这个能力包成外部可触发的桥。
安装
要求:DSH ≥
0.2.0-rc.2(更低版本未验证);dsh webprofile(host 平面插件,不需要 client 半边)。
方式 A:运行时注入(有 dsh-super-injector 时,免重启)
dev_inject_plugin /path/to/dsh-session-wake
方式 B:持久装配(官方 profile 机制,推荐)
git clone https://github.com/Asheep233/dsh-session-wake ~/.dsh/plugins/dsh-session-wake
bash ~/.dsh/plugins/dsh-session-wake/scripts/install-into-profile.sh # 默认 web profile
# 然后重启 dsh web
install-into-profile.sh 做三件幂等的事(先备份 package.json):写 dependencies(link:)、
追加 dsh.profile.bundles、建 node_modules 链接。注意:dsh plugin add 只装包、不会加进
bundles(bundle 插件必须出现在 dsh.profile.bundles 才会被组合),所以别漏这一步。
方式 C:手动装配(知道自己在做什么)
- 把本包装到
~/.dsh/profiles/<profile>/node_modules/dsh-session-wake(symlink 到源码目录即可) ~/.dsh/profiles/<profile>/package.json:
{
"dependencies": { "dsh-session-wake": "link:/path/to/dsh-session-wake" },
"dsh": { "profile": { "bundles": ["... 现有 bundles ...", "dsh-session-wake"] } }
}
- 重启
dsh web
用法
HTTP(推荐:即时、有返回)
# 唤醒:给会话投一条 user message
curl -s -X POST http://127.0.0.1:3080/wake \
-H 'content-type: application/json' \
-d '{"sessionId":"session-1173e241-25f1-4a25-96f5-b48db0a3026c","text":"磁盘告警,请处理"}'
# → {"ok":true,"accepted":true,"sessionId":"...","requestId":"...","ms":37}
# 状态:桥版本、心跳、处理计数、依赖的 Host API 形状
curl -s http://127.0.0.1:3080/wake
# 自检:只解析会话、不跑模型(dryRun)
curl -s -X POST http://127.0.0.1:3080/wake \
-H 'content-type: application/json' \
-d '{"sessionId":"session-...","dryRun":true}'
# → {"dryRun":true,"sessionId":"...","found":true,"preset":"standard","cwd":"/path","events":4920,"live":true}
请求字段:
| 字段 | 必填 | 说明 |
|---|---|---|
sessionId | 是* | 完整 session-<uuid>(可省略 session- 前缀);*省略时用配置的 defaultSessionId |
text | 否 | prompt 文本;省略时用配置的 defaultPrompt |
mode | 否 | "queue"(默认,忙时排队)/ "steer"(插进当前 turn) |
requestId | 否 | 幂等键:同一个 id 重试不会重复插入消息 |
dryRun | 否 | true 时只 inspect() 会话,不投递、不跑模型 |
路由由插件
ctx.webServer.register()自注册,不经过/api/*的 browser-trust 围栏, 因此外部进程可以直接调;dsh web 默认只监听127.0.0.1。
文件队列(dsh 挂了也不丢,起来补投)
默认目录 $DSH_HOME/wake-inbox(可配置 queueDir):
# .req:第一行 session id(省略则用 defaultSessionId),其余为 prompt
printf 'session-1173e241-25f1-4a25-96f5-b48db0a3026c\n磁盘告警,请处理\n' > "$DSH_HOME/wake-inbox/a.req"
# 或 .json
echo '{"sessionId":"session-1173e241-...","text":"构建完成,继续"}' > "$DSH_HOME/wake-inbox/b.json"
- 成功即删;失败移到
failed/<name>.<ts>.err;心跳写在wake-inbox/bridge.json - 轮询间隔
pollMs(默认 1500ms)——dsh 启动时会立即补投积压请求 .json兼容 BOM / CRLF(PowerShellSet-Content -Encoding UTF8的产物直接可用)
触发场景示例
- 构建/测试完成 → 唤醒 agent 继续下一步
- 磁盘/服务告警 → 唤醒会话处置(本项目来源场景)
- CI 状态变化、文件出现、进程退出码、定时器(替代/补充 schedule)
- 任意语言/任意进程,只要能发 HTTP 或写文件
配置
cordis.patch.yml 的 config(或运行时注入时传 config),全部有默认值:
| 键 | 默认 | 说明 |
|---|---|---|
queueDir | null → $DSH_HOME/wake-inbox | 文件队列目录(null = 运行时解析) |
pollMs | 1500 | 队列轮询间隔 |
heartbeatMs | 10000 | bridge.json 心跳间隔 |
logFile | /tmp/dsh-session-wake.log | 审计日志(追加) |
defaultSessionId | '' | 请求缺 sessionId 时的默认目标 |
defaultPrompt | 「外部唤醒:请检查最新状态与待办事项,并继续处理。」 | 请求缺 text 时的默认文本 |
http | true | 是否注册 HTTP 入口 |
httpPath | /wake | HTTP 路由路径(exact) |
httpToken | '' | 非空时要求 x-wake-token 头或 ?token= |
maxBodyBytes | 65536 | HTTP body 上限 |
archiveDone | false | 成功后是否留档到 done/ |
示例:
- insert:
- id: dsh-session-wake
name: dsh-session-wake
config:
http: true
httpPath: /wake
httpToken: 'change-me'
defaultSessionId: session-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
语义与实测
| 场景 | 行为 | 实测(DSH 0.2.0-rc.2) |
|---|---|---|
| 热会话(web 进程内有 Agent) | 直接投递,起新 turn | accepted 39ms |
| 冷会话(仅持久化) | 按记录 preset 自动 resume,再起 turn | accepted 123ms,turn/start 正常 |
| 忙碌会话(正在跑 turn) | queue 模式排队,不打断,当前 turn 结束后处理 | 忙时投递 1ms 接受,结束后新 turn 处理 |
| 会话不存在 | HTTP 500 / 文件落 failed/ | {"error":"session \"...\" not found"} |
设计约束:
- 不写任何会话自定义事件:第三方插件
session.append()自定义类型会让会话永久无法 resume (Session.append()无法设置ignorable,reader 见到未知类型必须拒绝重建)。本插件只走官方 prompt 收件箱。 - 单写者:同一个会话同时被两个进程驱动会
SessionAlreadyOwnedError/session/writer-held。 如果该会话正在被 headless 驱动,别再用本桥;反之亦然。 - 冷会话沿用记录的模型:若会话记录的 provider/model 已下线,turn 会以
NO_ADAPTER结束(机制本身正常)。
安全
- 服务默认只监听
127.0.0.1;HTTP 入口等价于「本机用户可发消息」,与 Web GUI 同级。 - 需要更强约束:配置
httpToken,请求带x-wake-token: <token>或?token=<token>。 - 不要把 dsh web 绑到公网再开这个入口(
dsh web --host 0.0.0.0)——那等于开放任意 prompt 注入。 - 文件队列目录权限按需收紧(默认在
$DSH_HOME下)。
兼容性与升级
依赖面刻意压到最小(零 npm 依赖、只 contact 两个 Host 服务):
| 依赖 | 风险 | 说明 |
|---|---|---|
profile dependencies + dsh.profile.bundles + cordis.patch.yml | 低 | 官方装配机制 |
inject: ['sessionController','webServer'] | 中 | 服务改名会让插件停在 pending(可观测:无心跳) |
ctx.sessionController.prompt(request, signal) | 低-中 | @Remote("prompt"),与浏览器客户端同一 RPC 契约 |
ctx.webServer.register({kind,path,handler}) | 低-中 | 官方 webserver 路由 API |
升级后一条命令自检:
bash scripts/post-upgrade-check.sh
# 7 项:dsh 版本 / web 监听 / 路由 / API 形状 / dryRun 解析目标会话(不花 token)/ 心跳 / 事件循环
GET /wake 的 api 字段会带出 {sessionController, prompt, promptArity, inspect, webServer, register};
探测不到会打 ERROR 日志。若只是 API 形状变了,改 lib/index.js 的 deliver() / installHttp() 即可。
故障排查
| 现象 | 原因 / 处理 |
|---|---|
curl /wake 404 | 插件没加载:确认 dsh.profile.bundles 含 dsh-session-wake,重启 dsh web;看 /tmp/dsh-session-wake.log |
日志 sessionController.prompt 不可用 | DSH 升级改了 Host API → 改 deliver();GET /wake 的 api 字段定位 |
session/writer-held | 该会话正被另一个进程(如 headless)驱动 → 换目标或停掉另一写者 |
| 文件请求不处理 | queueDir 与写入目录不一致(看 GET /wake 的 queueDir);failed/ 里有错误文件 |
| 心跳过期 | 插件卡死/未 apply;tail /tmp/dsh-session-wake.log |
卸载
- 从
~/.dsh/profiles/<profile>/package.json的dependencies、dsh.profile.bundles里删掉dsh-session-wake - 删
node_modules/dsh-session-wake链接 - 重启 dsh web
(有注入器时也可 dev_uninject_plugin dsh-session-wake 立即卸载运行时实例。)
目录结构
dsh-session-wake/
├── package.json # dsh.bundle.patch 指向 cordis.patch.yml(官方 bundle 插件格式)
├── cordis.patch.yml # insert 本插件行(bundle 层)
├── lib/index.js # 全部实现(零依赖,单文件)
├── scripts/
│ ├── install-into-profile.sh # 持久装配(deps + bundles + link,幂等带备份)
│ └── post-upgrade-check.sh # 升级自检(7 项)
└── examples/
├── curl-wake.sh # HTTP 触发示例
└── file-queue-wake.sh # 文件队列触发示例
实现要点:所有资源注册挂 ctx.effect(热重载/卸载自动清理);投递串行化避免并发抢同一会话;
文件按 rename 原子认领;同文件重试用内容 hash 做 requestId 幂等去重。
License
MIT
Comments
Loading…