Enosensu/dsh-fork-relink

DSH 的 fork 伴随插件:fork 后把原会话的子 agent 复制到新对话,并保持可继续发消息 / Copy a session's subagents onto a forked DSH conversation and keep them reachable. Co-developed with AI.

Project Overview项目介绍

This is a plugin for DeepSeek Harness. After forking a session, it automatically copies all direct sub-agents from the original session to the new forked one, leaving original sub-agent records unchanged. Use it when you need to retain full sub-agent information after forking a DSH session. Running sub-agents are skipped, so fork again after they finish.

这是DeepSeek Harness的分支重连插件,fork会话后自动将原会话的所有直接子agent完整复制到新分支会话,原会话子agent记录保持不变。当你需要fork DSH会话后保留完整子agent信息时可以使用它。注意:正在运行的子agent会被跳过,需等其结束后再次fork。

Or use CLI install (for developers)或使用命令行安装(适合开发者)

CLI Install命令行安装

dsh plugin --profile web add github:Enosensu/dsh-fork-relink

Enosensu/dsh-fork-relink 加入你的 DSH 配置(web profile)即可启用。

READMEREADME

dsh-fork-relink

DSH(DeepSeek Harness)官方 fork 的伴随插件:fork(分支)发生后,自动为原会话的直接子 agent 创建完整副本挂到新会话下——fork 出的对话完整保留子 agent 面板、代理路由、descriptor、label 等信息;原会话的子 agent 记录原样保留(复制而非转移,两个分支互不干扰)。

本插件由 Enosensu 与 AI(ZCode 智能体,GLM 模型)结对开发 · Co-developed with AI.

背景

DSH 的 fork(会话列表里的分支按钮)用新会话 id 承接被继承的历史,而子 agent 会话按 parentSession 挂在父会话下。fork 之后,子 agent 记录仍指向旧会话——新对话的子 agent 面板为空,子 agent 的全部工作记录"消失"(实际仍在磁盘上,只是脱离了新对话)。

原理

监听官方 session/created 事件。fork 子会话(isSeeded 且有 parentSessionorigin !== 'subagent')进入 store 时,为旧会话的每个直接子 agent 调用官方 agents.create 创建完整副本:

  • meta.parentSession 在创建那一刻就指向 fork 子会话(无文件改写、无归属抢夺);
  • meta.origin: 'subagent'delegationDepth = 父 +1;
  • seed = 原子 agent 的完整持久日志(逐行一致),inheritedEventCount = seed 长度;
  • agentOptions 取原子 agent descriptor 里的 provider/model;
  • setup 经官方 agentPresets.composeFrom(childCtx, forkChildAgent.ctx) 加入 fork 子会话的组合(与 spawn 路径同一入口);
  • 递归:副本的子 agent 同样被复制,整棵子 agent 树跟随;
  • 副本是冷会话:副本落盘后立即释放活体。留活的副本会持有该会话的写租约,而插件经 agents.create 创建的活体不在 subagent continuation manager 的 resident 表里 ⇒ send_message 不走活体投递、改走冷恢复,而冷恢复第一步 persistence.open(id, 'write') 会被副本自己的写租约拒绝(SessionAlreadyOwnedError),对外表现为 subagent "…" is unavailable —— 目录里看得见、消息发不进。释放后副本留在磁盘上,成为官方 resume 路径可寻址的冷会话,首次发消息由官方冷恢复按 descriptor 唤醒;
  • 过滤:运行中的子 agent 跳过(记入日志);fork 子会话 seed 未引用的子 agent(属于被分支抛弃的路线)不跟随;
  • 操作记录写入 $DSH_HOME/dsh-fork-relink.log,含每个副本的可寻址性复核结果(unresumable 为空即全部可寻址)。

可达性(明示)

对象 fork 前旧父 fork 后新父
原子 agent(原件) 仍然可达(记录与 id 均未改动) 不可达(UNAUTHORIZED: belongs to another parent session)
副本(新 id) 不可达(它不是旧父的子) 可达:list_agents 的 id 与 send_message 接受的 id 完全一致,首次发送冷恢复唤醒

语义是复制而非转移:旧分支照常使用原子 agent,新分支使用副本,两个分支互不干扰。错误码沿用核心词汇(NOT_RESUMABLE / UNAUTHORIZED / PARENT_UNAVAILABLE),插件不新增也不改写。

运维后果(必须知道的唯一一条):fork 之后,任何「续跑同一子会话」的协议都要改用副本 id——原 id 仍归旧分支,对新父必然返回 UNAUTHORIZED: belongs to another parent session。以 list_agents 给出的 id 为准,它与你应当发送的 id 是同一个。

子 agent 复制部分零依赖、零文件改写:全部走官方 API。任何走官方 fork 的入口(原生分支按钮、其他插件)都被覆盖。

安装

从 GitHub 安装(使用者走这条;装到的是本仓库 main 的当前代码,本插件是纯 ESM、无构建步骤,所以 git 安装不需要 allowBuilds 授权):

dsh plugin --profile web add github:Enosensu/dsh-fork-relink

本地开发时直接指向工作目录:

dsh plugin --profile web add "link:<本目录>"

重启 dsh web 生效。卸载:

dsh plugin --profile web remove dsh-fork-relink

测试

真机自测(在真实 web 服务器进程内驱动官方 sessionController.fork):fork 子会话的 session/created 事件触发、两个子 agent 的副本被创建(事件日志与原件逐行一致,仅多官方 seed 标记;头部 parent/origin/depth 正确)、原件不动;守卫三条(排除子 agent 自身创建/普通会话)与 seed 引用过滤均有离线测试覆盖。

继承的排队消息(补官方队列条之缺)

fork 会把原会话未消费的排队轮次(agent/inbox/splicednext-turn 折叠)一并继承下来。这些项在继续对话时会先于你新发的消息送达模型,而官方队列条并不总能显示它们:宿主只在 inbox 投影变化时推送队列帧,而 fork 子会话的投影在 agent 挂上之前就已水合 —— 那一帧可能被丢弃,队列于是要么迟到、要么根本不出现。

本插件只补官方条显示不到的行,不替换它:客户端读官方那条队列(useSession(s => s.queue)placement === 'queued' 的 id),把这批 id 从宿主侧折叠结果里减掉,差值非空才渲染。因此:

  • 官方条已经显示时,本插件什么都不画(实测:官方 3 行、本插件 0 行);
  • 官方条缺失或迟到时,继承项仍然可见、仍可编辑删除(实测过的历史情形,也正是本插件存在的理由);
  • 行集合与官方条同语义(只取 next-turn),不会把插话/上下文项冒充成"将先于新消息送达"。

路由(与官方 updateQueue 同一入口,要求会话在服务器内处于打开状态):

  • 读取 POST /log-prune/queue { sessionId }{ ok, items: [{ id, text, inherited }] }:活体会话读 Session.snapshotEvents(),冷会话读 session.v3.jsonl.zstd;活体读取失败自动回退文件,不让队列静默消失。
  • 编辑 POST /log-prune/queue/edit { sessionId, itemId, text } → 官方 edit(空文本按官方规则拒绝)。
  • 删除 POST /log-prune/queue/remove { sessionId, itemId } → 官方 remove

inherited 标记该项是否来自继承前缀:切点取日志里最后一条 session/end-seed { inherited: true }(fork 子会话在继承切点写入的标记),活体会话用精确的 inheritedEventCount

版式对齐官方队列条:提示条不引用官方组件(插件只能 require 客户端平台表里的 9 个共享模块,ui-conversation 与其 CSS 模块都不在其中),而是逐项复用同一套布局令牌与尺寸——--dsh-composer-card-max-width / --dsh-composer-dock-inset / --dsh-composer-side-clearance / --dsh-composer-stack-gap、36px 行高、12px 12px 0 0 面板圆角、--dsw-specific-tip 面板底色、28×28 圆形操作按钮、28px 输入态编辑器;实测宽度等于官方公式上限(card-max-width − 2×dock-inset),margin: 0 auto calc(0px - stack-gap - 3px) 与官方一致,因此与官方队列条叠放时读作同一块面。编辑交互同官方:铅笔按钮进入编辑态,Enter 保存 / Esc 取消,文本按全文读取(界面只用 CSS 省略号做显示截断)。

队列本身保持原样:插件只显示、编辑与手动删除,不自动清除。

已知边界

  • 运行中的子 agent 跳过(避免与内存态冲突),记入日志;可等它结束再 fork 一次。
  • 副本完整复制子 agent 的历史;若未来官方 fork 支持子树跟随(或提供 children relink/copy API),本插件即可卸载。

License

MIT

上一个 Prev harness-desktop 下一个 Next dsh-cliproxyapi-provider