zephyrrtos.cnZEPHYR RTOS 本土生态工作组

← 全部插件 · 当前:dsh-handoff

DSH 插件 · 会话交接 · 上下文耗尽兜底

dsh-handoff:上下文真的用尽时,换一条干净的会话接着干

典型场景:一次跑很久的任务——大型迁移、反复失败重试、无人值守的长目标——把上下文顶到窗口上限, 而同会话压缩已经压不动了:装了大型工具集的 envelope 本身就占掉一大块、一次超大工具调用是个不可分单元、 或者「摘要的摘要」逐轮失真。这时候你要的不是把历史摘要得更狠,而是换一条干净会话接着干, 并且说清楚为什么换、换过去的是什么。

它不自作主张。三重门同时打开才动手:压力真的到顶、观测到同会话压缩已经失败、agent 正处于空闲边界。 判定只用会话日志证据,不靠对服务的探测。

包产物 0.1.0(私有 Gitea registry,latest)· 许可 MIT · 零运行时依赖 · 分发:私有 registry / git 依赖(钉标签)· 源码仓库(私有,需账号或会员)

它解决什么:压缩救不回来的那四种情形

DSH 已经会在接近窗口上限时原地压缩(阈值 0.8 × 窗口),并在提供方确认溢出后压缩重试。这覆盖了绝大多数长会话。 本插件补的是它结构上覆盖不了的部分——这四条都写在压缩自己的 README 里,不是猜测:

情形为什么压缩没用
envelope 主导压缩只压缩派生历史,绝不缩减系统提示词、工具 schema 或会话前缀。装了大型 MCP 工具集时,那部分本身就吃掉可观比例。
不可分单元平衡摘要无法拆分一个不可分单元。一次超大工具结果、一条超长粘贴,压缩会直接判定「没有可压的区间」,什么都不做。
摘要的摘要每次压缩都把上次的摘要当普通历史再压一次。多轮之后信息逐级失真,而 token 仍会再次触顶。
收敛失败压缩重试用尽后只记一条警告,然后携带超预算历史继续——下一次请求就可能在提供方侧溢出。

交接的代价明显更高:一次摘要调用、一条新会话、以及用户的注意力被打断。所以默认立场是不交接, 只有压缩被证明无解时才动手。这也意味着它在正常使用里很少触发——稀有是设计正确的证据,不是没生效。

它怎么判断:三重门 + 护栏

门 1 · 压力

  • 已用 ≥ handoffRatio(默认 0.92)× 路由模型的 contextWindow
  • 该值在装载时被强制要求严格大于压缩的 0.8:设成小于等于会直接装载失败
  • 拿不到 contextWindow 就不动——不猜

门 2 · 压缩已无解

  • E1 无可压区间(超过压缩阈值却从未记录过任何 compaction/* 事件)
  • E3 连续压缩释放量低于窗口的 5%
  • E4 提供方已确认溢出,且此后没有产生任何持久进展
  • E5 一轮因 max-tokens 收尾且有活跃 goal(这正是驱动器解除续行的时刻)

门 3 · 边界

  • 只在 agent/status === idle 动手:没有 driver 在调度、没有打开的轮次
  • 链深达到 maxChainDepth(默认 8)拒绝继续接力
  • 后继会话若没产生任何 tool/result,不再往下接力

判定只用日志证据。这一点是被实测纠正过来的:曾经有一条「宿主层探不到压缩服务 ⇒ 没有后端」的证据, 而 dsh-web-app 模式恰恰会禁用宿主层的压缩行、把后端挂到 agent preset 上—— 探测结果永远是「缺失」,而压缩工作得好好的。那条证据单独就能放行,于是插件会去抢压缩的活, 正是整个设计要避免的那一个失败。现在它被删掉了,并有一条回归测试守着。

交接简报

后继会话是空上下文——这正是目的。它拿到的是第一份、也是唯一一份交接材料,由一条 <session-handoff> 包裹的消息投递:

① 模型摘要逐字回放前任的 system 与工具 schema,再加固定指令——这样辅助调用成为会话的真正前缀,提供方热前缀缓存可复用。八段结构:原始意图 / 当前工作 / 下一步 / 文件 / 错误 / 未完成 / 关键上下文 / 前任会话。
② 确定性回退模型调用失败时不放弃交接,而是从日志重建简报:最后一条用户输入、最近的助手输出、被读改过的文件、未完成的待办、未结清的审批、最近的错误。质量更差,但永远可用——而且「上下文满了」与「模型正在挣扎」是相关的。
③ 按优先级裁剪超出预算时先砍「错误与修复」「未完成任务」,最后才动别的;而「原始意图」与「下一步」永不丢弃——丢了它们,简报就不值得换一条会话,宁可放弃交接。

安装与接入

不在公共 npm 上:dsh-handoff 这个名字已被他人占用,所以用 scoped 名 @hakehuang/dsh-handoff,包放在私有 Gitea registry 上, 需要账号或会员;匿名请求返回 401。三种方式:

方式 A · 私有 registry(推荐)

# 一次性:让这个 scope 走私有源
npm config set @hakehuang:registry https://zephyrrtos.cn:3000/api/packages/hakehuang/npm/

# 装进 DSH profile(在 profile 目录里)
npm i @hakehuang/dsh-handoff

方式 B · git 依赖(无需 registry 凭据,需要仓库只读权限)

npm i "git+https://zephyrrtos.cn:3000/hakehuang/dsh-handoff.git#pkg-v0.1.0"

⚠️ git 依赖会把整个仓库交给消费者(git 不认 files 字段); registry 路径则只交出打包白名单里的 17 个文件。要边界就钉 registry,要方便就钉标签。

方式 C · 本地目录(开发用)

# profile 的 package.json
"dependencies": { "@hakehuang/dsh-handoff": "link:../../path/to/dsh-handoff" }

然后:把插件行插进 profile

# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
    - id: handoff
      name: '@hakehuang/dsh-handoff'
      config:
        handoffRatio: 0.92

⚠️ name 必须逐字等于包名。装载器按包名解析这一行,写错的话静默挂不上,不报错。 装完重启 DSH,日志里应出现:

handoff: successors will be created from the root context, so they survive this plugin being reloaded
handoff: ready — handoffRatio=0.92 maxChainDepth=8 mode=auto ledger=…

没有这两行就是没挂上。装上后可用 /handoff status 查看当前会话的实测判定与原因; /handoff 可以立刻交接一次(跳过压力判定,但不跳过护栏)。

配置

handoffRatio0.92 · 门 1 阈值,必须 > 压缩的 0.8,否则装载失败
minGainRatio0.05 · 单次压缩释放少于窗口这个比例算「停滞」
maxChainDepth8 · 一条谱系最多被接力多少次
maxBriefTokens8192 · 简报硬预算
allowDeterministicFallbacktrue · 模型摘要失败时退回确定性简报;设为 false 则宁可不交接
carryGoalfalse · 在新会话新建等价目标并武装续行。默认关:它会放大「跑偏」
toolFilternull · 可收紧后继会话的可用工具
modeauto · 自动交接;manual 只响应 /handoff

局限与未做的事(先读这节)

这一节照实写,因为「只讲优势不讲局限」正是本站点其他工具要防的那种失真。

怎么调这个阈值

  • 本机实测 contextWindow = 1,000,000,一条极长对话也才到 45%(约 44.9 万 token)
  • 所以 handoffRatio: 0.92 ≈ 92 万 token,约等于那条对话的两倍——在你的实际用法里它几乎不会触发
  • 希望它在长任务里真的接上手,有效区间是 0.82–0.88(不能低于 0.8,那是压缩的地盘)

它会拒绝交接的情况

  • 会话没有可继承的 cwd(后继会在侧栏看不见)
  • 接链已达上限、或后继没干过活
  • 简报即使裁剪后仍超预算
  • 用户在交接途中说话——这时中止,而不是两边同时写

还没做 / 不适用

  • 无客户端半边:交接徽章、横幅、自动切台都没实现
  • 只在一个 dsh-web-app profile 上实测过;其他模式未验证
  • 交接可能丢细节——关键状态应当写进工作区文件,而不是指望对话存活
  • E2(envelope 主导)尚未作为独立证据;本机 envelope 只占窗口 1.3%,它对这里几乎不生效

发布凭据的现状:目前用管理员账户密码发布,这是本项目自己登记过的治理问题 (建议换成只勾 write:package 的 Gitea 令牌)。这也意味着消费者拿到的是私有源,匿名不可装。

问智能体 Zephyr 问题问答 · 直接问智能体