DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-session-wake

d

dsh-session-wake

Manifest valid

Instant wake for running DSH sessions — a host-plane bridge using the official sessionController.prompt() API (HTTP + file queue). Why headless can't.

hasBundlePatch

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 web profile(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:手动装配(知道自己在做什么)

  1. 把本包装到 ~/.dsh/profiles/<profile>/node_modules/dsh-session-wake(symlink 到源码目录即可)
  2. ~/.dsh/profiles/<profile>/package.json:
{
  "dependencies": { "dsh-session-wake": "link:/path/to/dsh-session-wake" },
  "dsh": { "profile": { "bundles": ["... 现有 bundles ...", "dsh-session-wake"] } }
}
  1. 重启 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(PowerShell Set-Content -Encoding UTF8 的产物直接可用)

触发场景示例

  • 构建/测试完成 → 唤醒 agent 继续下一步
  • 磁盘/服务告警 → 唤醒会话处置(本项目来源场景)
  • CI 状态变化、文件出现、进程退出码、定时器(替代/补充 schedule)
  • 任意语言/任意进程,只要能发 HTTP 或写文件

配置

cordis.patch.yml 的 config(或运行时注入时传 config),全部有默认值:

键默认说明
queueDirnull → $DSH_HOME/wake-inbox文件队列目录(null = 运行时解析)
pollMs1500队列轮询间隔
heartbeatMs10000bridge.json 心跳间隔
logFile/tmp/dsh-session-wake.log审计日志(追加)
defaultSessionId''请求缺 sessionId 时的默认目标
defaultPrompt「外部唤醒:请检查最新状态与待办事项,并继续处理。」请求缺 text 时的默认文本
httptrue是否注册 HTTP 入口
httpPath/wakeHTTP 路由路径(exact)
httpToken''非空时要求 x-wake-token 头或 ?token=
maxBodyBytes65536HTTP body 上限
archiveDonefalse成功后是否留档到 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)直接投递,起新 turnaccepted 39ms
冷会话(仅持久化)按记录 preset 自动 resume,再起 turnaccepted 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

卸载

  1. 从 ~/.dsh/profiles/<profile>/package.json 的 dependencies、dsh.profile.bundles 里删掉 dsh-session-wake
  2. 删 node_modules/dsh-session-wake 链接
  3. 重启 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…