DeepSeek Harness 深度评测 · Ep3DEEP DIVE

Ep 3 DeepSeek Harness 深度评测 连载中

领域 Agent 与框架推理与效率

DeepSeek Harness Ep 3:热卸载——装上容易,卸下来要静默

2026-08-23 阅读约 15 分钟 正文约 6041 字

视频版:B站 公开投稿中无对应视频(已核对全部 16 个投稿) 视频总览 →

本期要点POINTS

本期要点

本期未收录要点:抽取阶段判定该篇正文没有可提炼的线索性结论(不造数、不空着)。

关键实测数据DATA

关键实测数据

本期无新增实测:该篇正文没有自测数字(不造数、不空着)。 同类读数集中在有实测的期与 证据索引页。

锐Owl · 解构系列 · DeepSeek Harness(Ep 3/5) 素材:2026-08-17 clone(commit 99f6f02)源码逐层拆解,全部论断带文件路径/行号


开篇:为什么拆「卸」

上一期拆完 skill,我们留下一个问题:装上容易,卸下来呢?

插件系统有个心照不宣的规矩——大家都爱讲「装」:市场、一键安装、生态多繁荣。没人爱讲「卸」:卸的时候会不会留垃圾?会不会打断正在跑的事?会不会把整个系统搞崩?

dsh 的答案不在文档里,在代码里:卸载不是一条命令,是一套生命周期纪律。 这一期从最容易被误解的 hooks 入手——它看起来像第八种扩展机制,其实是一座桥。


一、hooks 是桥,不是第八种机制

1.1 翻译层,不是扩展层

packages/hooks/README.md:5 定义得很清楚:hooks 子系统的职责是翻译外部 shell-hook 协议——把 Claude Code、Codex 的 hook 约定,「翻译」成 dsh 自己的类型化拦截点。它由两部分组成:一个共享线协议库(hook-protocol)+ 两个方言桥(hooks-claude-code / hooks-codex)。

dsh 的原生扩展面是 harness 自身的类型化拦截点(agent/pre-step、tools/pre-execute 等),所谓「原生 hook」只是挂在这些拦截点上的普通 Cordis 插件。hooks 桥做的一切,普通插件都能直接做,而且更强——设计笔记 agent/notes/implemented/feature/2026-06-30-interception-extension-points.md:9 把区别写得很直白:

维度 普通插件 bridge
返回值 类型化 Decision 解析后的中性 HookOutput 再映射成 Decision
上下文 全 ctx,无序列化边界 经 stdin/stdout 线协议
会话事件 不写 hook/* 事件 每次调用写 hook/invoked/hook/result 日志对

← 表格可左右滑动查看

为什么这个区分值得较真?因为「翻译层」和「扩展机制」的差别,决定了卸载时谁说了算——桥是普通插件,走标准装卸路径;特权机制才需要特殊照顾。dsh 这里没有特权核心。

1.2 两个方言桥

桥接双方各自只留方言差异(hooks-claude-code/src/config.ts:11-19、hooks-codex/src/config.ts:11):

  • Claude Code 桥:支持 7 个事件;握手=解析 hooks.json 的 hooks 键(src/index.ts:102-116),只跑 type:'command';解析期做 ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} 命令替换;PreToolUse 支持 deny/ask、PostToolUse 附 additionalContexts、Stop 阻塞时 agent.steer() 强制续跑(:241-277)。
  • Codex 桥:只支持 5 个事件;regex-only 匹配(:131),无命令替换;payload 全 snake_case、每个事件带 model、turn 事件带 turn_id(:292-308);只认阻塞——PreToolUse deny 即拒,无 allow/ask。

同一个协议,两个世界:CC 的 hook 生态偏「护栏」,Codex 的偏「门禁」。


二、线协议:一段三段式的执行链

hooks 的线协议(packages/hooks/hook-protocol/)是四个模块的流水线:

  1. 匹配(src/matcher.ts:57):absent / '' / '*' 全匹配;CC 模式对纯字面量按精确备选(| 分隔),其余按非锚定正则;Codex 永远按正则。非法正则运行时当不匹配(:64),解析期由 matcherDiagnostic 抛 SyntaxError 拒配(:37-44)。
  2. 执行(src/runner.ts:67):payload 经 JSON.stringify 写 stdin,经 ctx.shell 执行——继承凭据清洗、进程组取消、超时机制(:87);hook 级 timeoutSec 覆盖默认 10 分钟。基础设施失败被吞成 exitCode: undefined 的非阻塞错误,绝不 throw 进回合(:96-105)。
  3. 解码(src/codec.ts:59):退出码 2 = 阻塞,stderr 即 reason(:66-69);仅退出码 0 解析以 { 开头的 stdout JSON,畸形 JSON 宽容降级为纯文本;expectedEventName 守卫:事件字段不匹配整块丢弃(:122-124)。updatedInput 只解析不执行——外部的数据永远只是数据。
  4. 合并(src/merge.ts:62):同一点多个 hook 折叠成最严格结果,权限秩 deny(3) > ask(2) > allow(1)(:35-42),首个 continue:false 粘性停(:79-82)。

这段链的意义:外部协议被「翻译」进 dsh 时,错误边界、权限秩、类型校验都在翻译层完成,不是留给调用方碰运气。


三、卸载纪律一:生命周期即装卸

热卸载的第一条纪律:在 dsh 里,插件不是「安装/卸载」两个动作,而是「出生/死亡」两个时刻——出生时注册的一切能力,死亡时必须按逆序自动解除。

3.1 逆序 dispose

为什么逆序?因为依赖是有方向的:后注册的插件可能依赖先注册的服务,先卸掉被依赖的,依赖它的就会踩空。Cordis fiber 里所有监听经 ctx.on() 注册,listener 归当前 fiber 所有、返回 disposer(vendor/cordis/src/events.ts:90-97),fiber 卸载时按逆序自动移除——先卸最上层的,再卸底层的。

3.2 注册即撤销

这条纪律不是 hooks 特有的:上一期的 skill、更早的内核事件系统,全是同一套——registerProvider() 注册时返回「注销函数」,effect 销毁时自动注销(ep2 已拆)。一个框架敢说「一切皆插件」,前提是每个插件都带自己的撤销器,卸载即撤销,不靠运气。


四、卸载纪律二:静默——先杀、再等、才完成

第二条纪律才是「热卸载」里最硬核的部分。有些事件是「发出去就不管」的——比如会话启动信号(SessionStart),插件点了火,后续的异步链自己跑。问题来了:卸载的时候,这些尾巴还在跑怎么办?晚一步回调打进来,可能打在一个已经销毁的上下文上——那就是崩溃、数据错乱、幽灵进程(hook-protocol/README.md:26)。

dsh 的做法(hook-protocol/src/detached.ts:43):每个桥用 createDetachedRuns 追踪整条 run+continuation 链,并注册一个「排水器」drain():

drain():
  1. abort 追踪信号     → 杀掉还在跑的 hook 进程
  2. 循环 allSettled    → 等所有异步链落定
  3. 注册表空才 resolve → 卸载才算完成

drain() 作为 effect disposer 注册(hooks-claude-code/src/index.ts:126、hooks-codex/src/index.ts:105),卸载瞬间执行。注意这个顺序:先杀、再等、才宣告完成——不等直接卸 = 静默的炸弹;等了不杀 = 幽灵进程。两个都做,才是真静默。fiber.dispose() resolve 的那一刻,detached 工作已经彻底落定(hook-protocol/README.md:26)。


五、装卸的边界:什么不支持热更新

「热卸载」也有边界,说清楚才诚实。两个 hooks 桥的配置——hooks.json 那些——是进程级的:装载时读一次(hooks-claude-code/src/index.ts:102-116、hooks-codex/src/index.ts:86-97),之后换配置不生效,需要重载插件。代码里还留着 TODO:per-session 动态配置,还没做(hooks-claude-code/src/index.ts:50-51)。

这说明什么?dsh 的「热」是有范围的:

层 支持热卸载? 机制
插件实例 ✅ 动态装卸 fiber 生命周期 + 逆序 dispose
配置读取 ❌ 装载时快照 换配置需重载插件,TODO per-session

← 表格可左右滑动查看

插件实例可以动态装卸——这是生命周期机制;但配置读取是装载时快照——这是工程取舍。边界画得很清楚,不吹「全动态」。这个诚实,本身是工程成熟度的信号。


六、格局:卸载纪律是平台的分水岭

把三期的线索串起来:内核的注册即效果、skill 的卸载即撤销、今天的静默排水——是同一套纪律的三个面。

判断一个 agent 框架是不是真平台,别听它讲装得有多热闹,看它卸得干不干净:卸载会不会留垃圾、会不会崩、会不会留幽灵进程。dsh 连「卸载时正在跑的异步尾巴」都处理了——先杀、再等、才完成——这是冲着生产环境去的。玩具框架的典型特征恰好相反:装得热闹,卸靠重启。

四万星到十四万星,涨的是热度;热度背后的成色,是这种没人看见的纪律。

下一期:Typert——协议由编译器生成,不是人手写的文档。


来源表(引用红线)

# 论断 来源
1 hooks 是翻译层不是第八种扩展机制 packages/hooks/README.md:5;.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md:9
2 原生拦截点 agent/pre-step、tools/pre-execute 类型化 仓库核心拦截点定义(ep1 已拆)
3 桥接双方:CC 7 事件 / Codex 5 事件 hooks-claude-code/src/config.ts:11-19、hooks-codex/src/config.ts:11
4 线协议四模块:matcher / runner / codec / merge hook-protocol/src/matcher.ts:57、runner.ts:67、codec.ts:59、merge.ts:62
5 执行继承凭据清洗/进程组取消/超时;基础设施失败不 throw hook-protocol/src/runner.ts:87,96-105
6 退出码 2 阻塞、畸形 JSON 降级、expectedEventName 守卫 hook-protocol/src/codec.ts:59-89
7 权限秩 deny>ask>allow、continue:false 粘性停 hook-protocol/src/merge.ts:35-42,79-82
8 fiber 逆序 dispose / ctx.on 返回 disposer vendor/cordis/src/events.ts:90-97(ep1 已拆)
9 detached drain:abort → 循环 allSettled → 注册表空才 resolve hook-protocol/src/detached.ts:53-60
10 drain 作为 effect disposer 注册(卸载瞬间执行) hooks-claude-code/src/index.ts:126、hooks-codex/src/index.ts:105
11 晚回调打进已销毁 ctx 的防护 hook-protocol/README.md:26
12 配置进程级、装载时读一次、TODO per-session hooks-claude-code/src/index.ts:50-51,102-116、hooks-codex/src/index.ts:86-97
13 CC 方言:命令替换/steer 续跑/PreToolUse deny-ask hooks-claude-code/src/index.ts:147-180,241-277
14 Codex 方言:snake_case/regex-only/仅阻塞 hooks-codex/src/index.ts:131,146,229,292-308
15 玩具框架 vs 平台对比 分析层:由 [1]-[14] 机制证据归纳

← 表格可左右滑动查看

仓库版本:2026-08-17 clone,commit 99f6f02。制作前若隔日,重查 tags/commit 确认版本未漂移。

信号来源SIGNALS

信号来源

这一期从哪条雷达长出来(雷达 → 产出的双向可追溯):

node_links/radar_items.intent 的 feeds 指向关系当前为 0(M2 首轮回填统一先落 standalone,待人工指认)——回填后此处显示「雷达 YYYY-Www 条目 ③ → 已成为本期」。

来源表SOURCES

来源表

该篇正文里没有可解析的「来源」小节(早期篇目未写文末来源表)——不补造。