Bundle
dsh-command-queue
DSH 插件:让 /compact 等斜杠命令在 agent 忙碌时进入命令队列,空闲后自动执行;并在浏览器里以复刻原生队列的样式展示这份队列。
- Source
- kuyueliuhun-ctrl
- License
- MIT
- Updated
- Updated 20 hours ago
Readme
# 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.**
[](https://github.com/kuyueliuhun-ctrl/dsh-command-queue/releases)
[](./LICENSE)
[](https://github.com/topics/dsh-plugin)
[](https://github.com/deepseek-ai/deepseek-harness)
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`:
1. 命令**被排队**,输入框**不冻结**,你可以继续打字、继续排消息;
2. composer 上方、**原生消息队列正下方**立刻出现一条「命令队列」:
单条直接列出一行 `/compact`,多条折叠成「已排队命令 · N」可展开,每行带一个移除按钮;
3. 回合结束后命令真正执行,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(推荐,重启后仍在)
```bash
cd "$DSH_HOME/profiles/<profile>" # 例如 ~/.dsh/profiles/web
npm install github:kuyueliuhun-ctrl/dsh-command-queue
```
然后把包名加进该 profile `package.json` 的 `dsh.profile.bundles` 数组:
```json
{ "dsh": { "profile": { "bundles": ["dsh-command-queue"] } } }
```
本包自带 `cordis.patch.yml`,bundle 层会把 `command-queue` 这一行插进 loader,无需手写 patch 条目。
### 方式 B:本地目录装配(开发)
```bash
npm install "file:/path/to/dsh-command-queue"
```
用了 dsh-super-injector 的话:
```bash
dev_install_package { "dir": "/path/to/dsh-command-queue", "profile": "web" }
```
### 方式 C:运行时注入(免重启,临时验证)
```bash
dev_inject_plugin { "dir": "/path/to/dsh-command-queue" }
dev_plugin_status {}
```
### 卸载
```bash
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` 里对应条目下:
```yaml
- 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 是「同步 emit `agent/status` → `wakeDriver()`」,
任何跨 promise 边界后再调用都会撞回 running —— 所以「检查 idle」与「调用」必须落在**同一个同步块**里。
**为什么不用 DSH 原生队列?** 原生队列(agent inbox)的条目只能是 `UserMessage`(模型可见内容),
没有「命令项」类型;且队列项在**下一个 step 边界**就被 `claim()` 取走,那一刻 agent 仍在 running,
`compactNow` 照样 busy。即「一直显示在原生队列里」与「等 idle 才执行」互斥 —— 所以本插件
另建一份队列,只把**样式与位置**复刻成原生队列的样子。
📖 完整调用链、`文件:行` 证据、实测运行时行为与验证记录见 **[docs/INTERNALS.md](./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 空闲不会放行另一个在队的命令 |
## 开发
```bash
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.*`。
## 许可
[MIT](./LICENSE) · 问题与建议欢迎提到 [Issues](https://github.com/kuyueliuhun-ctrl/dsh-command-queue/issues)。
Install
dsh plugin --profile web add github:kuyueliuhun-ctrl/dsh-command-queue
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-command-queue from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.