DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-autofork

d

dsh-autofork

Manifest valid★ 2

Automatically fork sessions when the Agent is busy: your new commands get an immediate response in a fresh session, while the old session keeps running in the background and injects the results back (DeepSeek Harness plugin)

UI (client)hasBundlePatch

dsh-autofork

DSH 插件:agent 忙的时候你提交的新指令会立刻在新会话里得到响应,原来那条会话留在后台把活干完、再把结果回注过来。
Automatically forks the session when you type while the agent is busy — you keep talking, the old turn keeps working.

license format

这是什么

DSH 的自动分叉插件:你在 agent 忙的时候提交新指令,它会立刻把你放进一条新会话里继续说话;原来那条会话留在后台把这一轮跑完(绝不中止),跑完后把结果回注到你手上这条。

关键是"自动"。 官方 subagent / subagent_fork 要求 agent 自己决定要不要并行、fork 哪一段;这里的分叉是 harness 侧被动发生的 —— agent 不需要参与决策,也不会因此被打断。

为什么需要它

问题不在性能,在交互模型:一个 turn 常常十几分钟起步(调工具、长生成),而你现在只有 queue(等当前 turn 结束)或 steer(等下一个 step 边界)两条路,两者都是串行的——这期间你插不进话,一天就被切成很多段"什么都做不了"的碎片时间。

常见对策是同时开好几个 session:哪个在跑就切到另一个上读反馈、写下一条指令。但这要你自己记住每条会话在干什么——人的工作记忆做这种上下文切换很累,还会打断心流;而且精力有限,同时照看 3–5 条基本就到顶了。

dsh-autofork 要改的就是这个"必须等"的交互模型:把并行交给 harness,而不是交给你。你不必判断"要不要开子任务""该开几条""哪条在等",只要在想说话的时候说话——忙的那条自己在后台跑完、把结论交回来。要照看的会话数从"手动维护 3–5 条"降到"1 条"。

它不解决什么

它把"你被挡住"变成"你随时能说话",但不能消除"任务本身要等":真实工作常常有时间因果关系——下一条指令依赖当前这步的结论。那种等待是问题本身的性质,不是换一种交互模型能绕开的。

与相邻方案的区别

方案谁发起你要做什么
subagent / subagent_fork(官方)agent 自己决定并行、决定 fork 哪一段不用管,但得等它想到
agent-team(官方 0.1.5-alpha.2 起)管理者模式:共享任务区 + lead 身份 + 委派与通信理解层级与任务区
dsh-autoforkharness 被动发生:你插话就分叉什么都不用做,正常说话即可

没有管理者、没有层级:你始终在跟"当前这个 agent"说话,身后被接管的会话都在后台跑完、把结果交上来;需要时它才用 fork_list 看整个家族。

安装

dsh plugin --profile web add github:vlln/dsh-autofork

装完重启 web(bundle 走层栈)。从本机目录安装(开发用,要一条 symlink 修法)见工程笔记。

用起来是什么样

只在 agent 忙的时候分叉:它空闲时(能立刻响应你)就是它自己回答,不会平白多出一条会话;只有它正跑着自己那一轮、来不及被打断时,你发的那条指令才会带出一条新会话。

一次分叉你会看到:

你会看到说明
左侧列表冒出新会话名字是 ⑂1 <原会话名>;同族在列表里成组(⑂1 / ⑂2 … 共用一个基名,序号不累加)
焦点被切到新会话你立刻在跟一条空闲的 agent 说话;转写带着旧会话已完成部分,读起来是连续的
旧会话继续跑它这一轮不中止;跑完后把最后一条回复作为「收到的任务消息」卡片发到你现在这条会话
多出一个「分叉」页签本会话所在家族的血缘树:每行一条会话(名字 / 状态 / 触发它的那条指令 / 多久之前分的),点任意一行跳过去
新会话开头两张卡片「分叉通知」(谁接手、后台那条正在动哪些文件、避让要点)与「在飞进度」(后台那一轮的原始事件流水)以卡片出现在你那条指令上方,带图标、标题、时间和可展开正文。正文按「卡片上已经有的就不再重复」精简过
后台跑完的结果也是一张卡片旧会话那一轮结束时,它最后一条回复以同样的卡片落到你手上这条会话里,并真的唤醒它去读(你会看到它据此回一句)——不再是一行灰色的"上下文注入",也不是"卡片到了却没人理"
前两张卡不额外花请求分叉通知与在飞摘要和你的指令在同一轮里被认领(一次模型请求)。后台回传则是"真的来了一条消息",按官方 subagent 的语义会唤醒一轮——这一次请求是该花的

再忙一次会接着分叉(A → B → C):最新的那条负责回答你,身后的会话都在后台跑完、把结果交上来。已经被接管的会话里你再发言,消息直接交给最新那条(不再分叉)。

两种分叉方式

在侧栏 插件 → @vlln/dsh-autofork 的包页里选:卡上是两个选项卡,选中的那张有品牌色描边与圆点(改完立刻生效,不用重启、也没有保存按钮;选择写进 profile 的 cordis.patch.yml,重启后仍在):

方式你提交指令时发生什么
自动分叉(默认)agent 忙的时候,你提交的指令立刻在新会话里开始执行——本插件一直以来的行为
手动分叉指令照常入队(原会话继续跑它那一轮);队列那条指令的右侧多一个「分叉」按钮,点它才把这条指令拿出去开新会话

手动模式存在的理由:有些指令你想先在自己手上排队看清、或者本来就想让它排在当前这轮之后——"要不要分叉"由你按那一下决定,而不是一提交就分出去。两种方式只改何时分叉,分叉本身(继承的上下文、在飞摘要、结果回注、命名)完全一样。

按钮挂在哪:队列行右侧那几个按钮(编辑 / 删除 / 转向)由官方 ui-conversation 独占,插件层没有"往队列行加动作"的槽位。所以这个「分叉」按钮是用 DOM 门户注入队列那一行的——如果哪天 DSH 改了那块 DOM 结构,按钮会退回成对话框上方一条右对齐的窄条(每条排队指令一个按钮),功能不受影响。

家族里的通信是单向自动 + 双向显式

这是最容易想当然的一点,所以单独写出来:

  • 自动通道只有一条方向:后台会话(origin)那一轮跑完,会把最后一条回复作为卡片投给接手它的会话。 反向没有自动通道。
  • 因此在任一条会话里写普通回复,对方看不到;要跨会话说话只能用 fork_send(族内任意两条会话之间都行)。
  • fork_send 默认一发即走;要等对方答复就用 block: true(等它那一轮跑完并把回复带回来)。用 block: true 时,收方那条消息会明确标出"有人在等回复":正文里写「它在等你回复」,卡片上也挂一枚「等待回复」胶囊——免得对方把这条当纯信息收下。

注入给 agent 的文案(分叉通知 / 分叉提示)与工具描述里都写明了这三条 —— 否则 agent 会把自己的普通回复 当成已经发给了对方(实测踩过:B 以为"收到、我承接了"已经回给了 A,而那条文本只留在 B 自己的转写里)。

工具

agent 自己会用到的三个工具(exposeDispatchTools: false 可关掉):

工具说明
fork_list列出你家族里的其它会话:↑ 你接管的、↓ 接管了你的、~ 同族的另一条分叉。每行带名字与一个实时状态(运行中 / 空闲 / 已结束)
fork_send给家族里的任意一条会话发一条消息(等价 steer):它在当前 step 结束后读到并改向,不必中止。默认一发即走、没有回执;block: true 时等它那一轮跑完,把它的最后一条回复作为本次工具结果返回(上限 forkSendBlockTimeoutMs;超时只表示「还没回」,消息已经投递)
fork_cancel中止家族里的一条会话(keepInbox=true 只中止当前 turn、保留待处理项)

界面

位置说明
「分叉」页签会话视图区新增的一个 tab(与「对话」「轨迹」并列):本会话所在家族的树状图(节点卡 + S 形连线,与「对话」的转写同宽居中)。每张卡给三条信息——名字与序号徽章(⑂1,家族根是「根」)、此刻在干什么(只在还活着的卡上挂状态胶囊:运行中 / 空闲;已结束的卡整张变淡)、以及这次分叉是为什么、多久之前(触发指令 + 相对时间)。悬停任意一张卡会点亮它的血统(祖先链 + 自己的整棵子树),其余淡出;整张卡只有一个 hover 详情,且是分层浮层(标题行 / 键值行:完整 session id、触发指令、分叉时刻;标签列对齐,触发指令带引用块),鼠标在卡内移动不会换信息;点卡切到那条会话。画布铺满可用宽、不设上限,家族再大也只是横向滚动
给不在运行的会话发消息也行:fork_send 的目标此刻不在本进程(例如 dsh 重启过)时会先把它恢复再投递——恢复不了(正被另一个进程打开)才如实失败。所以家族通信不依赖"两边都还活着"。

让某条会话安静下来:「分叉」页签里每张卡右上角有一个小开关(绿/蓝=开,灰=关)。它管的是这条会话自己:跑完要不要自动把最后一条结果投给它派生出的会话——关掉一条,安静的是它的播报,别人的照旧。关掉之后会话照常运行、树照常显示、工具照常用,什么都不删(关系、历史、命名都保留),开关随会话落盘、重启后仍在。

| 会话名 | 分叉出来的会话保持家族根名不变,另外在侧栏行与「分叉」页签的卡片上显示一枚 ⑂n 徽标(标记不掺进名字里)(如 ⑂1 修 GUI 卡顿),同族在左侧列表里成组 | | 消息卡 | 分叉通知 / 在飞摘要 / 后台回注都以官方的跨 agent 消息(agent-message + form:'relay' + senderSessionId)落进新会话,再由本插件的客户端视图画成卡片(带图标、标题、时间、可展开正文) | | 注入行 | 容器模式的绑定通知、镜像行等仍以一行可折叠摘要出现(noticeSummaries: false 可关掉折叠) |

参数

全部行为参数集中声明在 src/params.mjs,无硬编码;插件配置覆盖任意字段,未提供者取默认值,未知键被忽略。

参数默认含义
enabledtrue总开关;关闭后完全退回 steer/queue
forkMode'auto'分叉方式:'auto' 忙时立即分叉;'manual' 指令先入队、点队列行上的「分叉」才分叉(见两种分叉方式)。唯一在 Plugins 页可改的字段
minStepAgeMs0分叉门槛:step 已运行低于此时长则不分叉。默认 0 = 只要忙就分叉(保守部署可调大)
coalesceWindowMs2000距下一个 step 边界不足此时长时降级为 steer(合并窗口)
forkableSourceKinds['user']只对这些 message.source.kind 触发分叉
maxActiveBranches3同一前台下的活跃后台分叉上限;超出强制 steer
harvestAfterMs600000已回注分叉多久后视为已收割并移出计数
digestMaxChars8000in-flight digest 整体字符上限
digestAssistantChars600单条助手文本截断长度
digestToolArgsChars300单条工具调用参数截断长度
digestToolResultChars400单条工具结果截断长度
digestMaxEvents200digest 最多渲染事件数(自尾部保留最近的;丢弃量在正文最前面标注)
reInjectOnTurnEndtrue后台 turn 结束后是否把结果作为一张卡片投给前台(不唤醒它,所以不会多一次请求)
reInjectMaxChars4000回注内容字符上限
containerModefalse容器模式(实验,默认关):容器不跑 turn、消息交给绑定实例、答复写成一等回复。不要打开:容器不可能完全不跑 turn,这条模式会与实例重复劳动
rebindNoticetrue容器模式下首次绑定 / 重绑 driver 时,在容器里插一行折叠通知
followHeadtrue是否把焦点切到新分叉。默认 true = 立刻在跟一条空闲 agent 说话(「打破同步交互」的前提);false 留作对照:只看产出、视图不移动
instanceLabel'分叉'实例在会话头部实例树里的显示名
titleMark'⑂'分叉会话的命名标记:建 head 时把标题钉成 <标记><序号> <家族根名>(如 ⑂1 修 GUI 卡顿)。序号不累加且按家族计数(不是按基名:同名的两个家族各算各的)——同族共用一个基名、各自编号,所以左侧列表里同族成组。走原生 sessionTitle.rename()(与手动重命名同一条路),代价是这条会话不再被自动命名。空串 = 关闭分叉命名
instanceProvider'fork'写进子会话 subagent/descriptor 的 provider(只用于冷恢复反查后端)
mirrorInstanceReplyfalse是否把实例的回复反向追加进容器日志。默认关:方向错了(笔记定的是"旧会话的结果注入回新会话"),那是 reInjectOnTurnEnd 干的
mirrorDelivery'safe'实例回复的投递方式:'safe' 避开悬空 tool_calls(必要时延到下一个 step 起点);'immediate' 直接 append(已知会写坏容器转写,仅作对照)
noticeSummariestrue注入消息是否声明 form:'notice'——客户端把它折叠成一行摘要(可展开),不折叠则整块显示
exposeDispatchToolstrue是否向前台暴露 fork_list / fork_send / fork_cancel
forkSendBlockTimeoutMs600000fork_send(block:true) 最多等对方多久(毫秒)。超时不是失败:消息已投递,结果文本会说明"还没回",让调用方改用 fork_list 看进展
debugLogPath''决策日志路径(逐行 JSON);空表示关闭,可用环境变量 DSH_AUTOFORK_DEBUG 兜底

渲染预算不足时,digest 显式标注丢弃量(因字符预算未渲染 / 更早的 N 个事件),不做静默截断。

环境变量

两个都不改出厂默认值,优先级低于插件配置:

# 覆盖任意参数(JSON 对象;非法 JSON 与非对象值一律忽略,绝不会让插件起不来)
DSH_AUTOFORK_PARAMS='{"minStepAgeMs":0}' dsh web
# 打开决策日志(逐行 JSON:每条判定与早退原因)。**"为什么没分叉"先看它**——早退默认是静默的
DSH_AUTOFORK_DEBUG=/tmp/dsh-autofork-debug.log dsh web

自检

GET /api/dsh-autofork/health[?sessionId=<id>] 返回插件参数、已注册工具与家族状态(nodes 就是「分叉」页签的数据源,driver 是当前回答你的那条会话)。

它也是"插件到底装上没有"的唯一可靠判据:DSH 的 ctx.logger.info 不写 stdout,dsh plugin list 也只转发安装器。

已知限制

  • 分叉出来的会话是普通会话,不会出现在原生「子代理」实例树里 —— 这换来的是"用户所在的会话不变、内容换成新的"。家族关系由「分叉」页签与 fork_list 表达,重启不丢。
  • 命名会钉住标题:走的是与手动重命名同一条路(侧栏 / 面包屑 / 搜索都正常),代价是这条会话不再被自动命名。titleMark: '' 可整个关掉。
  • 没有文件级隔离:分叉与旧会话共享同一个 cwd。write / edit 有 stale-version 守卫会拦住静默覆盖,但 bash 里跑的 git commit / rm / 重定向不受保护 —— 两个并发会话碰同一批文件时请自己协调。
  • containerMode(默认关)与 mirrorDelivery: 'immediate' 是实验/对照开关,不要打开:前者会让容器与分叉重复劳动,后者已知会写坏会话转写。
  • 客户端一半只在 web 平台生效(dsh.client.platform = web)。

验证

npm run verify = 语法 + 3 道静态门禁 + 150 项自证测试(digest 的确定性/有界性、各条拒绝路径、结果回注、血缘持久化与命名规则、工具的授权边界、「分叉」页签的渲染——用 React 桩渲染真实 bundle,断言用户看到的文案)。npm run preview:tab 可以不开浏览器先看一眼页签长什么样。

真 harness 的行为层 E2E 台在 tests/e2e/:它需要一个 dsh 源码检出(官方 mock LLM 不在 npm 闭包里),所以不进 npm run verify。改这个插件的内部机制与踩坑记录见工程笔记。

插件管理

已装插件可以用 plugin-registry 的薄控制台(浏览器面板)管理 profile 插件安装态(bundle 层栈 + insert 行 + 启停),无需手改配置:

dsh plugin --profile web add https://github.com/vlln/plugin-registry.git#path:/packages/plugin/console

变更记录

面向使用者的版本变更见 CHANGELOG.md(0.x 期间新增功能与行为变更进 minor)。 改了插件源码后需要重启 dsh web——host half 是 ESM 缓存的,刷新页面只会换客户端那半 (症状:界面是新的、行为还是旧的)。

许可

MIT

Comments

Loading…