ventisyn/dsh-agent-control

dsh-agent-control

DeepSeek Harness plugin: delete a session or a single turn through one confirmation flow and one set of failure codes

待分类deepseek-harnesssession-management

dsh-agent-control

DeepSeek Harness(DSH)插件:把三件会改变实例现状的破坏性操作——删除会话、删除一轮对话、重启或关闭进程——收进一个插件,用同一套确认与失败语义管理。

  • 删除会话:把一个会话不留残留地移除——磁盘目录(两种 id 拼写)、工作区记账(含归档/置顶)、投影缓存记录,以及它派生的子代理会话(递归,同样清目录与缓存)。fork 出来的会话是独立对话,不受影响。子代理里只要有一个还驻留在内存里,整次删除都会拒绝、什么都不动
    • 插件不碰的:别的插件自己存的数据(例如审批类插件的事件记录里可能提到这个会话 id)、按内容寻址共享的附件($DSH_HOME/attachments,可能被别的会话引用)
  • 删除一轮对话:从模型可见的上下文里移除一轮已结束的对话(问题、回复与该轮的工具记录),会话本身、之后的轮次与日志都保留
    • 做法与 DSH 自己的手动压缩同形:一笔独立压缩事务把那一轮的可见区间换成一句「这里原有一轮对话,已被用户删除」的提示,模型从此看不到原内容。只能在会话空闲时删(有一轮正在进行或正在压缩时会拒绝)。
    • ⚠️ v1.0.0 的旧写法(空的 system/message 墓碑)会让会话重启后打不开,已不再使用。
  • 热重启:把「DSH 进程重启」这件事也收进同一个插件——模型可以调用 restart_harness(必须说明原因)让需要重启才生效的改动(例如宿主插件代码)落地,重启后这个会话自动继续;用户也可以在设置页点按钮重启。见下方「热重启」一节。

插件列表里四行、可以逐个开关(core + 会话 / 轮次 / 热重启三个组件):关掉哪一行,那一个组件 就真的下线——路由消失、界面入口随之消失。见下方「组件开关」。

本插件只使用 DSH 的公开 API:不依赖内核的私有结构,也不照抄任何第三方插件的实现。这一点是刻意的——依赖私有结构的写法换一个 harness 版本就会静默失效(详见 AGENTS.md 第 4 节)。

两者的区别(最要紧的一条)

删除会话删除一轮
磁盘日志永久删除保留(日志是 append-only,不改写、不截断)
模型上下文会话整个消失那一轮不再进入上下文
附件随会话目录一起删不会清理(见下方「已知限制」)
可恢复❌ 不可恢复❌ 但内容仍在日志里

⚠️ 删除一轮不是安全删除。 它通过在日志末尾追加一笔压缩事务、把那一轮从模型可见面上替换掉来实现。被删内容仍然完整留在会话日志与附件存储里,只是不再进入模型上下文。任何「删了就没人能看到」的理解都是错的。

界面

  • 会话行「…」菜单 → 「删除会话…」(侧栏)
  • 每条已结束的回复旁 → 垃圾桶按钮(会话运行中时禁用)
  • 两者都会弹出同一个确认框,必须勾选确认才能点「删除」
  • 删掉的那一轮会在界面上被隐藏,让界面与模型看到的保持一致
  • 设置 → Agent 控制 → 运行状态、阻塞提示、「重启 DSH」按钮与「最近一次重启」卡片,外加一个「关闭实例」按钮(见下)

组件开关

本插件在插件列表里是四行(一行 core + 三个组件行),每一行都可以单独关掉。关掉之后那个组件 在本进程里根本不存在:路由消失、界面上的入口也跟着消失——不是「按钮还在、点了报错」。

行负责什么
dsh-agent-control(core)客户端 bundle 的交付、启动时清理孤儿归档标记,以及「按行构建各功能」的接线。它本身不含删除或重启能力
dsh-agent-control/session(会话)会话列表、删除会话(含只剩记账的「幽灵会话」清理),以及客户端判断这个组件在不在的只读探针
dsh-agent-control/turn(轮次)某个会话里已删轮次的列表、删除一轮,以及只读探针
dsh-agent-control/restart(热重启)重启状态 / 重启 / 关闭实例三条路由、模型工具 restart_harness、重启后的「续作投递」

怎么关:插件列表里本插件那一组「包含的组件」右侧的开关。关掉是一次 profile 写入 (宿主把那一行标成 disabled),重启实例后生效——host 端的接线只在进程启动时装配。

开关分两级:第一行是总闸(插件本体)。关掉它,另外三个组件拿不到本体提供的服务, 会停在未运行状态,一个接口都不会注册——界面也会跟着消失(客户端界面也由本体交付)。 本体开着时,三个组件可以各自独立开关。

关掉之后的实际影响:

  • 路由真的不见了:被关掉的组件那几条路由(含只读探针)在进程里根本不存在,请求会落到 404 或宿主的鉴权 fallback。GET /api/agent-control/sessions 这类探针可以直接用来确认。
  • 界面入口跟着消失:客户端在挂载时先探测这几个组件(GET /api/agent-control/session/status、 .../turn/status、.../restart/status,都是只读、不带参数、不改变状态的接口),探不到就不注册 对应的槽位——会话行菜单里的删除项、回复旁的垃圾桶、已删轮次的隐藏标记、设置页那一节、重启/关闭横幅与确认框。 判据是「先探测、后注册」(已经注册的槽位无法在运行中卸载),探测失败或超时的方向是少显示 (模型侧本来也有 host 闸门,少显示只是界面滞后)。
  • 旧配置不受影响:如果某个组件行从来不存在(例如只有一行 agent-control 的老配置),core 会把 三个组件全装上,行为与从前完全一致。
  • 关掉不删数据、也不改变已知残留:插件自己不落任何数据文件;会话删除在内存 store 里那条记录 (已知残留,见「已知限制」)仍然要等进程退出才消失——关掉组件既不会撤销已经发生的删除,也不会顺手清残留。
  • 关掉「热重启」= 重启与关闭都下线:restart_harness 工具、设置页那一节、界面上的重启按钮与 关闭实例按钮属于同一个组件,要留着它们就别关这一行。
  • 关掉一个组件不会让实例起不来(刻意的:配置写错一个词的代价不该是打不开)。

热重启与关闭实例

模型可以在需要的时候重启宿主进程(例如它刚改了本插件的 host 端代码,而 host 模块不会热加载),重启在本轮结束后执行,这个会话随后自动继续——模型会收到一条「DSH 已热重启」的通知,里面带着它自己写下的原因与续作说明。

  • 两个入口,同一套护栏:模型工具 restart_harness({ reason, resume_note? }) 与设置页的「重启 DSH」按钮走的是同一条路径(requestRestart)。
  • 模型发起时默认要过审批:审批卡片里显示模型给的原因;被拒就什么都不做。可以在插件行配置里把 approval 设成 auto 免审批(仍受限频与阻塞项约束)。
  • 有别的会话在跑、或有后台任务在跑时会拒绝,并告诉模型「等哪个会话结束」。这是刻意的:重启会中断那些任务。
    • ⚠️ 已知限制:宿主的作业列表按所有者隔离,插件看不到别的会话启动的后台任务,只能统计无主作业与调用者自己的作业。
  • 限频:10 分钟内最多 3 次;同一个会话在 60 秒内再次请求会被拒绝(防模型死循环)。
  • 停机时间约 6–10 秒,其中大部分是新进程启动的时间;重启期间页面会短暂不可用(不需要手动刷新,DSH 客户端会自己恢复)。
  • 不做配置回滚:如果新进程因为配置或插件错误起不来,插件不会替你改回任何东西——它会如实报告失败,并在设置页给出新进程日志的文件名,剩下的要你自己处理。
  • ⚠️ 如果你的实例是由桌面启动器(DSHL)拉起的:实测(v1.1.0)重启本身能成功,辅助进程也不会被杀,但启动器认不出重启后的新进程——它会从启动器的列表里消失,变成一个它管不到的孤儿进程(还活着、还在服务)。
    • 原因:启动器按它自己 spawn 的那个子进程 PID 认实例(就绪判定读的是子进程 stdout 里的登录横幅),进程一退出它就记「已退出」;它只在自己启动时做一次「核验并恢复监控」,判据仍是那个进程的身份,所以新 PID 不会被认领。
    • 后果与处置:启动器里看不到、也停不掉它(要从任务管理器结束进程);也别对同一个 profile 再点一次「启动」,那会变成两个实例争同一个端口。要恢复管理,只能先手动结束那个进程,再从启动器启动。
    • 这条本插件修不了:需要启动器提供重启接口,而本插件按设计不依赖、也不改写启动器的私有状态(那套记录是它的私有格式,靠它等于把插件绑死在一个第三方实现上)。
  • ⚠️ 端口暴露的风险同样适用:POST /api/agent-control/restart 走的是插件自建路由(没有 harness 鉴权),所以它额外做了 Origin/自定义头校验,避免被网页 CSRF。但如果你的 webServer 绑在 0.0.0.0 上,等于把「重启我的实例」暴露给整个网络——不要这么部署。

关闭实例

设置页同一页里还有一个「关闭 DSH」按钮:干净地停掉整个实例(ctx.appExit(0),与直接关窗口等价)。

  • 它存在的理由:实例被启动器失去跟踪之后(见上面那条),启动器里既看不到也停不掉它——这个按钮就是那种情况下唯一的收尾入口。当然,任何时候都能用它停实例。
  • 只有界面能触发:插件不提供对应的模型工具。关掉实例会让所有会话与后台任务一起中断,而且不会自动重启(不像热重启会续作),所以这个决定只能由人来做。
  • 必须勾选确认,弹窗里会列出当前有几个会话 / 后台任务会被中断,并写明「不会自动重启」。
  • 行为:接口先回 202(已接受这次关闭请求),然后进程退出;界面显示「DSH 正在关闭…」,随后是「实例已关闭,可以关掉这个页面」。要再用它,得从启动器或终端重新启动。
  • 不会写坏日志:退出前会尽力把每个会话已经记录的事件刷到盘上(每个会话最多等 500 毫秒,刷不动也照常退出)。如果关的时候正好有一轮在跑,那一轮在日志里会是「未闭合」,打开会话时由 DSH 内核按「被中断」补齐——这正是关窗口/崩溃时的同一套恢复路径,不是损坏。
  • 与热重启互斥:任一流程进行中时另一个按钮是禁用的。关闭不等正在跑的任务结束(它就是要立刻停),这一点与热重启刻意不同。

已知限制

  • 打开过的会话也能删(0.2.1-alpha.2-v1.2.0 起)。DSH 仍然没有把会话从内存 store 摘除的公开接口,但实测(0.2.1-alpha.2)活会话的磁盘目录删得掉、也不会被写回来,于是插件改成:先停掉 agent,再照常删目录、工作区记账与投影缓存;如果删的正是界面上正在看的那个会话,界面会切到同一工作区里的另一个会话。
    • 残留只有一处、并且说清楚:内存里那条 store 记录会留到进程退出(它不再对应任何磁盘数据,也不会再被界面打开);接口响应里用 wasLive: true 如实标注。
  • 删除一轮不清理附件:内核当前没有公开的附件清理接口。被删轮次里的图片/文件仍留在 $DSH_HOME/attachments。
  • 已压缩的轮次可能无法单独删除:如果那一轮与其它内容共用了一个压缩后的可见节点,或它的可见节点不再连续,插件会拒绝并说明原因,而不是删掉一半。
  • HTTP 接口没有 harness 鉴权:插件自建的路由不走 harness 的鉴权链路。会改变状态的 POST 路由(删除会话、删除轮次、重启、关闭)自己做了可信校验(优先用宿主的 requestRejection,不可用时靠同源 Origin / 自定义头兜底)并要求 content-type: application/json,别的网页不能跨站触发;但能访问到这个端口的非浏览器调用者带上自定义头仍可能调用。默认只监听回环地址;如果你的 webServer 配置成 0.0.0.0,请自行评估。

安装

# 装最新发布版:不写 ref 时取仓库默认分支,本仓库的默认分支始终指向最新发布版本
dsh plugin --profile <profile 名> add github:ventisyn/dsh-agent-control

# 需要钉到某个版本时,把 <完整版本号> 换成版本分支名(也是 Release 标题)
dsh plugin --profile <profile 名> add github:ventisyn/dsh-agent-control#<完整版本号>

# 本地开发(改完重启即生效)
dsh plugin --profile <profile 名> add link:<本地 clone 路径>

⚠️ 如果 profile 里已经装了别的删除类插件,请先移除,否则界面上会出现两套删除按钮——同一批槽位只能有一个主人:

dsh plugin --profile <profile 名> remove <占用了同名槽位的插件>

安装或改动 host 端代码后需要重启 dsh web:已加载的 host 模块不会因为文件改动而重新导入(新增的路由会一直返回 401)。

排障

装好之后:

# 会话列表(同时也是「路由是否注册成功」的探针)
curl http://127.0.0.1:<端口>/api/agent-control/sessions

# 热重启的状态(也是辅助进程判断「新进程起来了没有」的探针)
curl http://127.0.0.1:<端口>/api/agent-control/restart/status

浏览器控制台里:

window.__dshAgentControl.usingNativePrimitives()  // 用的是原生原语还是自带兜底
window.__dshAgentControl.id                       // 注册 id
window.__dshAgentControl.features()               // 这次挂载探测到的组件开关:{ session, turn, restart }
                                                  // true / false / null(null = 还没探到,按不可用处理)

接口一览:

GET  /api/agent-control/session/status    { ok: true, enabled: true } ←「会话」组件的只读探针(界面靠它决定注册不注册入口)
GET  /api/agent-control/sessions          会话列表
GET  /api/agent-control/turns?sessionId=  某个会话里已被删除的轮次
POST /api/agent-control/session/delete    { sessionId }
POST /api/agent-control/turn/delete       { sessionId, assistantMessageId }
GET  /api/agent-control/turn/status       「轮次」组件的只读探针(同上)
GET  /api/agent-control/restart/status    热重启的状态(也是新进程的就绪探针,同时是「重启」组件的只读探针)
POST /api/agent-control/restart           { reason?, force? } → 202 { ok, restartId }
POST /api/agent-control/shutdown          { }(可空体)→ 202 { ok } ← 停掉整个实例,只从界面触发

组件被关掉时,属于它的那几条路由根本不存在(请求落到 404 或宿主的鉴权 fallback), 所以「这个组件在不在」用上面那几条 /status 探针一拉就知道。

失败一律返回 { ok: false, error: { code, message } }。错误码含义:

码含义
REQUEST_DENIED删除请求没通过可信校验(Origin / 自定义头 / 浏览器鉴权),403;宿主鉴权给 401 时照抄 401
TARGET_NOT_FOUND目标已经不存在
TURN_NOT_CLOSED那一轮还没结束
TURN_COMPACTED那一轮已被压缩或不再连续,无法单独删除
AGENT_BUSY会话的任务没有停下来,已放弃删除
DELETE_FAILED宿主拒绝了这次删除(消息里带原始原因)
RESTART_BLOCKED别的会话或后台任务在忙(消息里列出是哪些)
RESTART_IN_PROGRESS已经有一次重启在进行中
RESTART_RATE_LIMITED太频繁(10 分钟 3 次 / 同会话 60 秒冷却)
RESTART_UNSUPPORTED这个部署做不到(取不到启动规格、没有 appExit、辅助脚本缺失…)——不会假装成功
RESTART_DENIED审批被拒,或这次请求没通过可信校验(Origin / 自定义头)
RESTART_FORBIDDEN子代理发起重启:只有主会话可以重启宿主
SHUTDOWN_DENIED关闭请求没通过可信校验(Origin / 自定义头 / 浏览器鉴权)
SHUTDOWN_UNSUPPORTED这个部署没有 appExit,插件无法请求退出(请从启动器或终端停止)
SHUTDOWN_FAILED请求宿主退出时抛错(消息里带原始原因)——不会假装已经关掉

开发

npm test    # 语法检查 + 314 项离线测试,不需要 DSH 进程,也不碰真实 profile

设计与取舍、与 DSH 版本的耦合点、改完的自检清单都在 AGENTS.md; 路由与事件细则在 docs/CONTRACT.md,已经踩过的坑在 docs/PITFALLS.md。

说明

  • 非官方:这是第三方插件,与 DeepSeek 官方没有隶属或背书关系;文中出现的 DeepSeek、DeepSeek Harness、DSH 等名称仅用于说明它在什么环境里运行。
  • 主要由 AI 编码代理编写:代码、测试与文档都是在与 AI 代理协作下产出的,提交历史里能直接看到。仓库里所有结论都尽量附上可复核的证据(日志、接口响应、诊断脚本输出),请按证据判断,不要按措辞判断。
  • 本插件的实测记录(包括未验证项)都写在仓库内 docs/VERIFY-*.md 里,请按那里的结论判断它的成熟度,不要只看本页的概览。
  • 细则与坑在 docs/ 下:CONTRACT.md(路由与事件)、PITFALLS.md、RELEASING.md。

许可

MIT