dsh-command-queue
让
/compact这类斜杠命令在 agent 忙碌时不再立刻报错,而是进入命令队列, 等 agent 空闲后自动执行 —— 并在浏览器里以复刻原生消息队列的样式展示这份队列。
Queue slash commands until the agent is idle — with a browser queue panel styled after DSH's native message queue.
DeepSeek Harness(DSH)插件 · host + client 双面 · 不改 DSH 源码 · 卸载即完全还原
问题:agent 忙碌时 /compact 只会报错
DSH 里普通消息和斜杠命令走两条完全不同的通道:
| 普通消息 | 斜杠命令 | |
|---|---|---|
| 客户端 | session.prompt({ mode }) |
remote.commands.execute() |
| host 落点 | agent.followup() / agent.steer() → agent inbox |
ctx.commands.execute() → 直接调 handler |
| 忙碌时 | 天然排队,回合结束再消费 | 立即执行,没有任何排队语义 |
/compact 的 handler 调 compactNow() → agent.runMaintenance(),后者在 agent 非 idle 时
同步抛 ManualCompactionError('busy')。于是 agent 正在跑的时候敲 /compact,只会立刻收到:
Compaction is unavailable because this process has an active compaction,
or the agent is not idle.
既没有排队,也没有重试。DSH 作者把「命令自己进队列」当作有意排除的方案记录在案 (Agent Note: "Queue the command itself. Rejected."),所以官方没有开关 —— 这正是本插件补上的那一段。
效果
agent 忙碌时敲 /compact:
- 命令被排队,输入框不冻结,你可以继续打字、继续排消息;
- composer 上方、原生消息队列正下方立刻出现一条「命令队列」:
单条直接列出一行
/compact,多条折叠成「已排队命令 · N」可展开,每行带一个移除按钮; - 回合结束后命令真正执行,transcript 里出现正常的命令卡片 (「执行中…」→「已完成 · Compacted N history items」),队列里那行同时消失。
特性
| 🧩 通用 | 任何斜杠命令都适用,不限于 /compact —— 不枚举命令名,拦截点在 ctx.commands.execute() |
| 👀 看得见 | 浏览器队列面板,CSS 与 DOM 取自原生 QueueDock 原文(仅换类名前缀),沿用全局主题变量 |
| 🔒 零回归 | 非命令语法 / 注册表解析不到 / 控制动词(/stop 等)/ agent 已空闲 —— 一律照旧立即执行 |
| 🧯 不吞命令 | 排队逻辑自身抛错时回落原行为;卸载时唤醒等待者让在队命令立刻执行完 |
| 🎛 可控 | 按 agent 隔离、严格 FIFO 串行、可随 UI 请求取消、可从面板移除 |
| 🔐 鉴权 | 面板数据走 /api 已鉴权路由(Host/Origin 围栏 + 签名 cookie),不用裸 webServer 路由 |
| ♻️ 可卸载 | 精确还原 execute(含原描述符),不留悬挂 RPC |
安装
本插件是手写 ESM / 浏览器 bundle,没有编译步骤;lib/index.js 与 lib/client.js 就是源码,
npm run build 只做语法校验 + 跑测试。
本包是 host + client 双面插件。 host 半边做排队与执行,client 半边 (
exports["./client"],由package.json的dsh.client.platform: "web"声明)渲染队列面板。 ⚠️ 客户端插件是页面加载时装配的 —— 安装/注入后需要刷新页面才会出现面板。
方式 A:从 GitHub 装配进 profile(推荐,重启后仍在)
cd "$DSH_HOME/profiles/<profile>" # 例如 ~/.dsh/profiles/web
npm install github:kuyueliuhun-ctrl/dsh-command-queue
然后把包名加进该 profile package.json 的 dsh.profile.bundles 数组:
{ "dsh": { "profile": { "bundles": ["dsh-command-queue"] } } }
本包自带 cordis.patch.yml,bundle 层会把 command-queue 这一行插进 loader,无需手写 patch 条目。
方式 B:本地目录装配(开发)
npm install "file:/path/to/dsh-command-queue"
用了 dsh-super-injector 的话:
dev_install_package { "dir": "/path/to/dsh-command-queue", "profile": "web" }
方式 C:运行时注入(免重启,临时验证)
dev_inject_plugin { "dir": "/path/to/dsh-command-queue" }
dev_plugin_status {}
卸载
dev_uninject_plugin { "match": "dsh-command-queue" }
# 非注入器环境:从 dsh.profile.bundles 移除该名字,再删除 node_modules 里的包
使用
浏览器队列面板
挂在原生消息队列同一个槽位 conversation.input.dock 上
(原生 id:"queue" / order:20,本插件 id:"command-queue" / order:30),
所以两条队列上下并排。队列为空时面板完全不渲染(返回 null),零视觉占用。
面板每 1.5s 轮询一次 host 快照;行右侧的按钮可以把一条还没执行的在队命令移除 (被移除的命令不会执行)。
/cmdqueue 命令(纯文本路径)
不想开浏览器面板时,用这个只读命令查看同一份队列:
/cmdqueue
→ 2 command(s) queued for this agent, waiting for it to become idle: /compact, /export
→ No queued commands for this agent.
行为契约:什么时候不排队
以下情况一律保持原行为(立即执行),保证零回归:
- 不是合法命令语法 —— 第 0 字节不是
/、名称含大写或非法字符(如/Compact)、只有/; - 命令名在注册表里解析不到 —— 客户端本来就会按「未命中」处理,没必要拖到空闲之后;
- 命令名在立即执行白名单里 —— 默认
stop/cancel/abort/interrupt/halt, 忙碌时敲/stop本该立刻中断,排队会失去意义甚至危险; - agent 当前就是 idle;
- 排队逻辑自身抛错 —— 兜底回落成原行为,绝不吞掉用户命令。
只有「合法 + 已注册 + 未豁免 + agent 正忙」才入队。
配置
无 schema,容错读取,全部可选。写在 profile 的 cordis.patch.yml 里对应条目下:
- id: command-queue
name: 'dsh-command-queue'
config:
verbose: true # 打开排队/执行日志(默认 false)
pollIntervalMs: 250 # host 侧等空闲的轮询兜底间隔,50–5000(默认 250)
exposeState: true # 注册给浏览器面板的已鉴权路由(默认 true;false = 纯 host)
immediateCommands: # 整体替换默认白名单
- stop
- cancel
- abort
alwaysImmediateCommands: # 在(默认或自定义的)白名单上追加
- my-control-command
它是怎么做到的
一句话:在 host 侧包装 ctx.commands.execute() —— agent 忙时把这次调用挂起,
等 agent/status 回到 idle 后调用原方法。命令仍由原注册表解析、仍由 CommandRuntime
写 command/run + command/done 生命周期日志,只是执行时机被推迟到空闲窗口。
有两个不查代码就会踩的坑(都与 DSH 内部实现有关):
ctx.commands是 cordis 的 traceable Proxy,service.execute === patched这种身份比较 恒为 false(get trap 对函数每次返回新的 shadow proxy)—— 用它做还原守卫会导致补丁永远摘不掉;AgentLoop.kick()的 finally 是「同步 emitagent/status→wakeDriver()」, 任何跨 promise 边界后再调用都会撞回 running —— 所以「检查 idle」与「调用」必须落在同一个同步块里。
为什么不用 DSH 原生队列? 原生队列(agent inbox)的条目只能是 UserMessage(模型可见内容),
没有「命令项」类型;且队列项在下一个 step 边界就被 claim() 取走,那一刻 agent 仍在 running,
compactNow 照样 busy。即「一直显示在原生队列里」与「等 idle 才执行」互斥 —— 所以本插件
另建一份队列,只把样式与位置复刻成原生队列的样子。
📖 完整调用链、文件:行 证据、实测运行时行为与验证记录见 docs/INTERNALS.md。
故障排查
| 现象 | 原因 / 处理 |
|---|---|
| 刷新了也看不到面板 | 客户端插件只在页面加载时装配。先确认 dev_plugin_status 里本插件是 client ✓;若显示 client ✗,是 pkgMeta 负缓存(见下一行),重启 host 或清缓存 |
| 从 v0.1.0 升级后 client 一直是 ✗ | dsh-client-modules 的 resolveMeta() 有 pkgMeta 负缓存:包在没有 dsh.client 声明的形态下加载过一次,null 就被缓存。重启 host 即可;全新安装不受影响 |
| 排队期间 transcript 里没提示 | 设计如此:command/run 事件在 handler 调用前才写入,而我们在它之前就挂住了。提示在队列面板里,不在 transcript |
| 命令一直不执行 | 它在等 agent 空闲。agent 若一直有排队消息,空闲窗口会被不断占用 —— 用 /cmdqueue 或面板确认它还在队列里 |
/stop 也进了队列 |
不该发生 —— 控制动词在默认白名单里。若你自定义过 immediateCommands(整体替换),记得把控制动词加回去 |
| 不同 agent 互相影响 | 不会。队列按 agent 隔离,一个 agent 空闲不会放行另一个在队的命令 |
开发
npm test # node --test tests/*.test.mjs
npm run check # node --check lib/index.js && node --check lib/client.js
npm run build # 语法校验 + 全套测试
| 测试文件 | 覆盖 |
|---|---|
tests/queue.test.mjs |
host 半边:立即放行(未空闲 / 未注册 / 非命令 / 白名单)、忙碌挂起后执行、FIFO 串行、多 agent 隔离、取消、卸载不丢命令、轮询兜底、异常回落、traceable Proxy 的 patch 与精确还原、两条 client 路由 |
tests/client-bundle.test.mjs |
浏览器半边:用最小 React 运行时 + 假 window.__ModuleLoader__ 真正加载并渲染 bundle —— bundle 形态(id / factory / 无 default)、dock 条目 id 与 order、异常自吞、空队列渲染 null、有命令时渲染并能经 drop 路由移除 |
第二道测试不是可选项:DSH 的 boot 审计没有 per-plugin 隔离, 客户端 bundle 抛错会让整个 Web 应用无法 mount。
兼容性
针对运行中的 @deepseek-ai/dsh@0.1.6-alpha.1 实现,依据其编译产物逐行核对。
只使用公开面:ctx.commands.execute/find/register、agent.status、
agent.ctx.on('agent/status')、ctx.connection.fetch.register、ctx.slots.*。
No comments yet. Be the first to write one.