@zfdx123/dsh-hooks-ordering 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/client.js +619 -0
  4. package/cordis.patch.yml +38 -0
  5. package/lib/dag-Bqx-sl71.d.ts +55 -0
  6. package/lib/dag-Bqx-sl71.d.ts.map +1 -0
  7. package/lib/dag-DVhoBjBG.js +48 -0
  8. package/lib/dag-DVhoBjBG.js.map +1 -0
  9. package/lib/dag.d.ts +3 -0
  10. package/lib/dag.js +3 -0
  11. package/lib/index.d.ts +121 -0
  12. package/lib/index.d.ts.map +1 -0
  13. package/lib/index.js +190 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/serial-Cu7usHjI.js +65 -0
  16. package/lib/serial-Cu7usHjI.js.map +1 -0
  17. package/lib/serial-D8ZJCBKL.d.ts +55 -0
  18. package/lib/serial-D8ZJCBKL.d.ts.map +1 -0
  19. package/lib/serial.d.ts +5 -0
  20. package/lib/serial.js +6 -0
  21. package/lib/service-base-CCmIBwnB.d.ts +100 -0
  22. package/lib/service-base-CCmIBwnB.d.ts.map +1 -0
  23. package/lib/service-base-a5vKg62S.js +139 -0
  24. package/lib/service-base-a5vKg62S.js.map +1 -0
  25. package/lib/service-base.d.ts +4 -0
  26. package/lib/service-base.js +5 -0
  27. package/lib/topo-sort-BZ1fFcTs.d.ts +54 -0
  28. package/lib/topo-sort-BZ1fFcTs.d.ts.map +1 -0
  29. package/lib/topo-sort-CfwYPY4U.js +83 -0
  30. package/lib/topo-sort-CfwYPY4U.js.map +1 -0
  31. package/lib/topo-sort.d.ts +2 -0
  32. package/lib/topo-sort.js +3 -0
  33. package/lib/waterfall-Bu6m9gYc.js +83 -0
  34. package/lib/waterfall-Bu6m9gYc.js.map +1 -0
  35. package/lib/waterfall-_5HkptkS.d.ts +87 -0
  36. package/lib/waterfall-_5HkptkS.d.ts.map +1 -0
  37. package/lib/waterfall.d.ts +5 -0
  38. package/lib/waterfall.js +6 -0
  39. package/package.json +117 -0
  40. package/src/dag.ts +92 -0
  41. package/src/dsh.ts +181 -0
  42. package/src/index.ts +54 -0
  43. package/src/serial.ts +108 -0
  44. package/src/service-base.ts +179 -0
  45. package/src/settings.ts +97 -0
  46. package/src/topo-sort.ts +118 -0
  47. package/src/waterfall.ts +153 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-hooks-ordering contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @zfdx123/dsh-hooks-ordering
2
+
3
+ 为 [Cordis](https://github.com/cordiverse/cordis) 钩子提供确定性的 `before`/`after` 排序:钩子的参与者由相互独立、彼此无感知的插件贡献,`waterfall` 与 `serial` 两种派发方式都支持,并附带可选的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 层,开箱即可控制真实的 dsh 钩子。它解决的问题很具体——Cordis 按**注册顺序**派发 waterfall 监听器(也就是它们在内部监听器数组中的位置,`prepend` 是唯一可用的调节手段),而注册顺序又由 `inject` 依赖的激活时机决定,**互不相关的插件之间的激活顺序是不确定的**,于是「认证先于日志、净化器先于序列化器、指标最后执行」这类真正要紧的顺序其实悄悄依赖于没人控制的加载顺序,改一个看似无关的 `inject` 就会翻转。本包**不需要**修改或 fork Cordis:它是一个普通 Cordis 插件,**包住**指定的钩子并自行决定参与者顺序,参与者注册到协调器上(而不是原始钩子),用 `before`/`after` 名字声明约束,再由一个稳定的拓扑排序定序——与插件加载时机无关。当前版本 1.0.1,面向 DSH `^0.1.6-alpha.1`。
4
+
5
+ ## 安装
6
+
7
+ 在 dsh profile 中把它作为 **bundle** 安装——`dsh` 会读取 `dsh.bundle.patch` 清单字段,并自行把该包注册进 `dsh.profile.bundles`:
8
+
9
+ ```sh
10
+ # 从 npm 安装单个包
11
+ dsh plugin --profile web add @zfdx123/dsh-hooks-ordering
12
+
13
+ # 一次装齐整套(MCP 管理器、技能管理器、记忆、CodeGraph、钩子排序、会话清理、Superpowers)
14
+ dsh plugin --profile web add @zfdx123/dsh-atelier
15
+
16
+ # 从本地检出安装(本地开发)
17
+ dsh plugin --profile <profile> add link:/absolute/path/to/dsh-hooks-ordering
18
+ ```
19
+
20
+ 装完**重启 dsh**(bundle 不做热加载),设置页里就会出现「钩子排序」。
21
+
22
+ **不要再手动把这一行插进 profile 的 `cordis.patch.yml`。** `dsh` 已经会从 bundle 自带的 patch 里插入它,重复一份会导致启动失败:`duplicate loader entry id: hooks-ordering`。
23
+
24
+ 在 dsh 之外,本包就是一个普通的 Cordis 插件:`pnpm add` 之后直接使用根入口、`/waterfall` 或 `/serial`。`@deepseek-ai/cordis` 是 peer dependency。
25
+
26
+ ## 快速上手
27
+
28
+ ### Waterfall 钩子
29
+
30
+ ```ts
31
+ import HookOrdering from '@zfdx123/dsh-hooks-ordering/waterfall'
32
+
33
+ ctx.plugin(HookOrdering)
34
+
35
+ // 钩子的拥有者(或应用组装方)只接管一次。这里安装唯一的包夹监听器;
36
+ // 接管两次会抛错,所以 prepend 竞态不会再回来。
37
+ ctx.hooksOrdering.control('request/assemble')
38
+
39
+ // 厂商 A——只声明自己的约束,不从厂商 B 导入任何东西。
40
+ ctx.hooksOrdering.register('request/assemble', 'front', {
41
+ name: 'auth',
42
+ before: ['logging'],
43
+ run: (req) => authenticate(req),
44
+ })
45
+
46
+ // 厂商 B——另一个包,对 A 一无所知。
47
+ ctx.hooksOrdering.register('request/assemble', 'front', {
48
+ name: 'logging',
49
+ run: (req) => log(req),
50
+ })
51
+
52
+ // 厂商 C——必须在一切之后运行,包括宿主默认行为。
53
+ ctx.hooksOrdering.register('request/assemble', 'back', {
54
+ name: 'metrics',
55
+ run: (req) => emitMetrics(req),
56
+ })
57
+ ```
58
+
59
+ 无论这三个插件以什么顺序加载或注册,`auth` 都在 `logging` 之前运行,`metrics` 最后运行:
60
+
61
+ ```
62
+ raw ctx.on, load order [auth, logging, metrics] -> auth, logging, metrics
63
+ raw ctx.on, load order [metrics, logging, auth] -> metrics, logging, auth ← 同一批插件,顺序翻转
64
+ HookOrdering, load order [auth, logging, metrics] -> auth, logging, <宿主默认>, metrics
65
+ HookOrdering, load order [metrics, logging, auth] -> auth, logging, <宿主默认>, metrics ← 稳定
66
+ ```
67
+
68
+ ### Serial 钩子
69
+
70
+ ```ts
71
+ import { SerialHookOrdering } from '@zfdx123/dsh-hooks-ordering'
72
+
73
+ ctx.plugin(SerialHookOrdering)
74
+ ctx.serialHooksOrdering.control('turn/stopping')
75
+
76
+ // front:先于原生链运行。返回一个 bail 值(除 null/false/undefined 之外的任何值)
77
+ // 会短路整次 serial 派发。
78
+ ctx.serialHooksOrdering.register('turn/stopping', 'front', {
79
+ name: 'guard',
80
+ run: (turn) => (isAllowed(turn) ? undefined : 'DENIED'),
81
+ })
82
+
83
+ // back:尽力最后执行(见「已知限制」)。
84
+ ctx.serialHooksOrdering.register('turn/stopping', 'back', {
85
+ name: 'audit',
86
+ run: (turn) => recordAudit(turn),
87
+ })
88
+ ```
89
+
90
+ ### 记录约束 DAG
91
+
92
+ 传入一个 `log` 文件,即可在**每次注册变化**时写出约束图(JSON),因此它始终反映当前状态——在排查意外的顺序或环时非常好用:
93
+
94
+ ```ts
95
+ ctx.plugin(HookOrdering, { log: './hooks-ordering-dag.json' })
96
+ ```
97
+
98
+ ```jsonc
99
+ {
100
+ "sections": [
101
+ {
102
+ "hook": "request/assemble",
103
+ "phase": "front",
104
+ "nodes": ["auth", "logging"],
105
+ "edges": [{ "from": "auth", "to": "logging" }] // auth 在 logging 之前运行
106
+ },
107
+ {
108
+ "hook": "request/assemble",
109
+ "phase": "back",
110
+ "nodes": ["metrics"],
111
+ "edges": []
112
+ }
113
+ ]
114
+ }
115
+ ```
116
+
117
+ 该图是**不做**拓扑排序直接渲染的,所以出现环时会被如实画出,而不是抛错。你还可以随时通过 `ctx.hooksOrdering.dumpDag()`(返回 JSON 字符串)以编程方式读取它。写入失败只会经 `console.warn` 报告,绝不会抛回 fiber。
118
+
119
+ ### 在 dsh 里怎么用
120
+
121
+ 装成 bundle(见「安装」)之后什么都不用做:dsh 层会自动接管默认的那批钩子,并在设置页「钩子排序」里暴露配置。控制一个没有任何参与者的钩子是**透明的直通**,所以这一行在某个插件用 `before`/`after` 注册之前不会改变任何行为。
122
+
123
+ ## 它做什么
124
+
125
+ - **限制排序范围、把顺序决定权交给协调器。** `HookOrdering` 用一个 `prepend` 过的监听器包住钩子,并利用洋葱模型:监听器 `next()` **之前**的代码先于整条原生链执行——**`front`** 阶段;`next()` **之后**的代码在一切之后执行,包括钩子自带的内置默认行为——**`back`** 阶段。
126
+ - **serial 用双协调器。** `SerialHookOrdering` 没有 `next()` 可以包裹,所以它用一个 `prepend` 过的 **front** 协调器先于原生链执行(在这里 bail 会短路整次派发),以及一个追加在末尾的 **back** 协调器,尽力最后执行。
127
+ - **稳定拓扑排序。** 并列时保持输入顺序;未知的 `before`/`after` 目标是无操作;出现环时抛 `OrderingCycleError` 并点名被阻塞的条目。
128
+ - **约束图可查。** `dumpDag()` 把每个受控钩子的约束图渲染成普通对象(`{ sections: [{ hook, phase, nodes, edges }] }`);配置 `log` 后每次注册变化都会写文件,失败只 `console.warn`。
129
+ - **控制真实的 dsh 钩子。** 根入口是一个 dsh 插件,挂载上述两个服务并接管那些被多个包共同贡献、**且返回契约扛得住异步包夹**的 dsh 钩子:默认 10 个 waterfall 钩子(`agent/pre-step`、`agent/request`、`agent/request-error`、`system-prompt/assemble`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`fs/write-intent`、`fs/edit-intent`、`approval/request`)与 1 个 serial 钩子(`agent/turn-stopping`)。这些名字都对着安装版本核过:每一个都是真实派发的事件,且派发方都会 `await`,所以包夹的异步阶段不会改变钩子的返回类型。
130
+ - **带一个浏览器设置页。** 页面名「钩子排序」,命名空间 `hooks-ordering`;客户端那一半优先使用外壳自带的 UI 原子(`Button`/`Input`/`Tag`/`StateDot`),拿不到时整体退回插件自绘的元素,不会白屏。
131
+ - **可选择的使用高度**(三层,互不依赖):
132
+
133
+ | 层 | 入口 | 提供什么 |
134
+ | --- | --- | --- |
135
+ | 1. 算法 | `@zfdx123/dsh-hooks-ordering/topo-sort`、`/dag` | 纯的稳定拓扑排序,以及约束图(JSON)渲染器。零依赖,不涉及 Cordis。 |
136
+ | 2. Cordis 服务 | `@zfdx123/dsh-hooks-ordering/waterfall`、`/serial` | `HookOrdering` 与 `SerialHookOrdering`——可控制任意 Cordis 应用中的任意钩子。 |
137
+ | 3. DeepSeek-Harness | `@zfdx123/dsh-hooks-ordering`(根入口,即插件) | 一个 dsh 插件 + `cordis.patch.yml`,替你控制真实的 dsh 钩子,外加一个浏览器设置页。 |
138
+
139
+ - **为什么做成插件而不是 Cordis 内核的一部分。** Cordis 刻意保持精简:它提供排序的**原语**(数组位置、`prepend`、`next()` 链)。排序的**策略**——数值序号、`before`/`after`、拓扑排序——因钩子而异,不是内核该关心的事。做成插件意味着零框架修改,也没有需要长期维护的 fork。
140
+
141
+ ### API
142
+
143
+ `ctx.hooksOrdering` —— `HookOrdering` 服务(waterfall):
144
+
145
+ | 方法 | 说明 |
146
+ | --- | --- |
147
+ | `control(hook)` | 在 waterfall 钩子 `hook` 上安装包夹。每个钩子调用一次。返回一个 disposer。若已被控制则抛 `HookControlError`。 |
148
+ | `register(hook, phase, entry)` | 往 `'front'` 或 `'back'` 添加一个参与者。返回一个 disposer。若该钩子未被控制、或被列在 `syncReturnHooks` 里(返回值会被调用方同步消费)则抛 `HookControlError`。 |
149
+ | `plan(hook, phase)` | 按它们将运行的顺序返回参与者名字——用于测试和诊断。 |
150
+ | `dumpDag()` | 以 JSON 字符串返回每个受控钩子的约束 DAG。 |
151
+
152
+ 配置:`ctx.plugin(HookOrdering, { log?: string, syncReturnHooks?: readonly string[] })`。
153
+
154
+ `ctx.serialHooksOrdering` —— `SerialHookOrdering` 服务(serial)表面相同(`control` / `register` / `plan` / `dumpDag`,同样的 `log` 配置)。其条目的 `run` 可以**返回**一个值:一个 bail 值(除 `null`/`false`/`undefined` 之外的任何值)会短路 serial 派发,并成为它的返回值。
155
+
156
+ `HookEntry` / `SerialHookEntry`:
157
+
158
+ | 字段 | 含义 |
159
+ | --- | --- |
160
+ | `name` | 在同一个 `(hook, phase)` 内唯一。供其他条目的 `before`/`after` 引用。 |
161
+ | `before?` | 本条目必须先于的名字。 |
162
+ | `after?` | 本条目必须后于的名字。 |
163
+ | `run(...payload)` | 以钩子负载调用(waterfall:派发参数,不含 Cordis 末尾的 `next`)。会被 await。serial 的 `run` 可以返回 bail 值。 |
164
+
165
+ `topoSort(entries)`(`.../topo-sort`)与 `buildDag(sections)`(`.../dag`)是零依赖的独立导出:前者是稳定拓扑排序,后者把约束图渲染成普通对象以便 `JSON.stringify`——它从不排序,也永不因环而抛错。
166
+
167
+ ## 配置
168
+
169
+ 三个字段可以直接在 dsh 的设置面板里编辑,页面名为左侧导航中的**钩子排序**(命名空间 `hooks-ordering`),无需改 profile:
170
+
171
+ | 字段 | 含义 |
172
+ | --- | --- |
173
+ | `hooks` | 要控制的 waterfall 钩子,一行一个。`[]` 完全禁用 waterfall 服务。 |
174
+ | `serialHooks` | 要控制的 serial 钩子,一行一个。`[]` 完全禁用 serial 服务。 |
175
+ | `log` | 约束 DAG(JSON)日志文件;留空表示「不记录」。顺序一旦看着不对就能派上用场。 |
176
+
177
+ 第四个组装键 `syncReturnHooks` **刻意不进设置页**:它描述的是*宿主*的派发方式(哪些钩子的返回值被同步消费),而不是用户的偏好,所以它只属于 profile 那一行。表单里给 `hooks` 填上一个同步返回的钩子是安全的——它会被控制,而 `register()` 会在插件试图注册参与者时拒绝,并说明原因。profile 行长这样(本包自带的 `cordis.patch.yml` 就是这个内容,只有在该包**没有**作为 bundle 注册时才需要手写):
178
+
179
+ ```yaml
180
+ - insert:
181
+ - id: hooks-ordering
182
+ # 裸包名——见下面的说明。`/dsh` 也能作为插件入口,
183
+ # 但那样 dsh 就找不到浏览器端那一半了。
184
+ name: '@zfdx123/dsh-hooks-ordering'
185
+ config:
186
+ # hooks: ['agent/pre-step', 'tools/post-execute'] # 默认:所有返回契约兼容的 dsh waterfall 钩子
187
+ # serialHooks: ['agent/turn-stopping'] # 默认:[agent/turn-stopping]
188
+ # syncReturnHooks: ['llm/stream', 'session-telemetry/record', 'compaction/summary-error']
189
+ # log: './hooks-ordering-dag.json' # 可选的 DAG 日志
190
+ ```
191
+
192
+ **这一行必须写包名,不能写子路径。** dsh 会把某一行的 `name` 映射回一个包,以便找到该包的浏览器端那一半(`dsh.client` → 设置页),而它的 `locatePkgJson` 只接受裸包标识符——`exactPackageSpecifier('@scope/name/subpath')` 返回 undefined,因为切分后得到三段。写成 `@zfdx123/dsh-hooks-ordering/dsh` 的那一行能完美加载宿主插件,然后悄悄地永远找不到客户端 bundle:没有设置页,而且任何地方都没有报错。这就是插件表面放在根入口的原因。
193
+
194
+ **出于同一类原因,根入口不带 `default` 导出。** 加载器会先用 `exports.default ?? exports` 规范化导入的模块,然后才应用它,所以一个并非该插件本身的 default 导出会劫持这一行:本包当时把 default(waterfall 服务)挂成了插件,`apply` 从未运行,结果是服务活着、却没有控制任何钩子,没有 serial 服务,没有设置命名空间,而且依然悄无声息。`HookOrdering` 从根入口按名字导出,同时仍然是 `/waterfall` 的默认导出。
195
+
196
+ 关于接线方式,还有四点值得知道:
197
+
198
+ - **profile 那一行是*基础*层。** 解析顺序是 schema 默认值 → `base`(这一行;当这一行为空时是内置钩子集合)→ 用户层。所以表单是在你的组装配置**之上**编辑,「恢复组合默认」会回到组装配置,而不是回到空表单。
199
+ - **编辑在重启后生效**(`applies: 'restart'`),这是有意为之:改 `hooks`/`serialHooks` 意味着在活动钩子上安装或移除包夹监听器,重启能干净地应用这些变更,而不是在派发链运行中途重新接线。设置 `log` 本身很廉价,但命名空间是作为一个整体生效的。
200
+ - **这个页面是客户端那一半的贡献。** 宿主侧的 `settings.register` 只创建命名空间、它的存储和它的描述符;dsh 从浏览器平面渲染设置,所以是 `client.js` 把页面注册进 `settings.section` 插槽(`order: 27`)。移除或加载失败客户端那一半,命名空间依然存在——只是没有表单。
201
+ - **`settings` 是硬依赖**(`export const inject = ['settings']`),这是承重的,而非偶然:cordis 会在一个插件声明的服务存在时立刻激活它,而 `ctx.get('settings')` 只是读取服务存储,**不会**建立那个需求。只做探测而不声明,意味着插件会在第一波加载中、宿主提供 `settings` 之前就加载,命名空间于是从未被注册——悄无声息,没有表单也没有报错。声明它同时也让 prepend 的包夹在启动流程中落得更晚,而排序保证正希望它们在那里。注册本身是降级而不是让挂载失败:如果它抛错,插件会告警并回退到组装配置,因为一个可选的表单绝不能阻止 harness 启动。
202
+
203
+ **本包面向 dsh。** 钩子名是 dsh 的,`settings` 是 dsh 的服务,所以两者都不做成可选的;`/waterfall` 和 `/serial` 才是不带宿主服务要求的入口。
204
+
205
+ ## 前置要求
206
+
207
+ - DeepSeek Harness `^0.1.6-alpha.1`(`engines.dsh`)
208
+ - Node `^22.19.0 || >=24.0.0`
209
+ - peer `@deepseek-ai/cordis ^4.0.2`;dsh 层另外声明可选的 peer `@deepseek-ai/dsh-settings ^0.1.6-alpha.1`
210
+ - 设置页需要 web profile(`dsh.client.platform = web`)与外壳的客户端组件表;拿不到原生组件时表单逐处降级,但页面本身仍需客户端那一半
211
+
212
+ ## 已知限制
213
+
214
+ - **仅支持 waterfall 与 serial。** waterfall 包夹需要 `next()`;serial 用两个带 bail 语义的协调器。`emit`/`parallel`/`bail` 钩子没有可协调的有序链。
215
+ - **受控钩子在没有任何参与者时,返回值不受影响。** Cordis 的 `waterfall` 是*同步*返回最外层监听器的值的,所以当两个阶段都为空时包夹会把整条链直接透传,该钩子的返回类型和以前完全一样。这正是「控制一个钩子是透明的」的原因。
216
+ - **一旦钩子有了参与者,它的返回值就变成一个 promise。** 包夹必须 await 它的两个阶段,所以一个调用方**不** await 就直接消费结果的钩子无法承载参与者。1.0.0 的 `syncReturnHooks` 默认集是**三个**钩子,它们因此被排除在默认控制集之外,并且 `register()` 会**拒绝**它们(而不是让返回值悄悄换型)。早先的修订只列了前两个,第三个是后加的:
217
+ - `llm/stream`——`dsh-llm` 用 `return this.ctx.waterfall(this, 'llm/stream', options, …)` 派发(不 await),`dsh-session-title-llm` 直接 `for await (const chunk of ctx.llm.stream(options))` 迭代;返回 promise 会让它在 `not async iterable` 上失败。这是个真损失:该钩子在安装版本里有多个互相独立的贡献者(`dsh-agent-loop`、`dsh-llm` 自己的 invariant、`dsh-session-checkpoint-policy`、`dsh-session-title`),正是本插件存在的理由——但排序它需要 dsh 改为 await 派发,而不是插件侧的绕道。
218
+ - `session-telemetry/record`——`dsh-session-telemetry` 把 waterfall 的返回值直接交给后端,promise 会被当成记录发出去:**静默数据错误,哪里都不报错**。
219
+ - `compaction/summary-error`——`dsh-compaction-basic` 用 `recover: (…) => this.ctx.waterfall('compaction/summary-error', …, () => false)` 派发(不 await),并在 `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error` 里**同步**消费那个布尔值。promise 永远为真,所以 `!recover(…)` 永远为假,**汇总失败被静默吞掉**、不再抛出:压缩会当作「恢复成功」继续走下去,而不是把错误暴露出来。
220
+
221
+ 这三个名字由 `syncReturnHooks` 配置给出,`control()` 仍然允许它们(包夹是透明直通),被拒的是 `register()`:拒绝发生在决策点,且带明确原因。等宿主开始 await 某个钩子后,把 `syncReturnHooks` 覆盖成不含它的集合即可接管。
222
+ - **每个钩子一个协调器。** 第二次 `prepend` 会重新引入竞态,所以 `control` 会拒绝重复接管。
223
+ - **它只排序它拥有的东西。** 协调器掌控自己的 `front`/`back` 注册表及其内部顺序,并把它们相对原生链放置。它不会在外部监听器**彼此之间**重新排序。
224
+ - **waterfall 的 `back` 是精确的;serial 的 `back` 是尽力而为的。** waterfall 的 `back` 通过 `next()` 在整个原生链之后运行。serial 没有 `next()`,所以它的 `back` 协调器在 `control()` 时被追加,并在那时已存在的监听器之后运行——在 `control()` **之后**新增的原生监听器会排在它后面,而任何 bail(原生的或 front 的)都会完全跳过它。
225
+ - **未知引用 = 无操作。** 跨厂商的 `after: ['maybe-absent']` 在那个对端未加载时不施加任何约束——跨厂商插件不能假设彼此的存在。
226
+ - **环在派发时响亮失败。** 约束冲突会抛 `OrderingCycleError`,并点名被阻塞的条目(同时 `dumpDag()` 仍会把这个环渲染出来供排查)。
227
+ - **协调器的包夹必须在原生监听器注册之后才被 prepend,所以请最后挂载本插件。** 插件用一个 **prepend** 过的监听器包住它控制的每个钩子;`prepend` 把它放到*到目前为止*已注册的所有监听器之前。最后挂载时,它的 `next()` 就包住了整条原生链——`front` 先于所有原生监听器运行,`back` 在所有原生监听器之后(以及宿主默认行为之后)运行。如果更早挂载,一个稍后用 `{ prepend: true }` 注册的原生插件会落到包夹*之前*并逃逸排序——它「覆盖」了协调器的位置。注意这里说的是*加载位置*,参与者不受加载顺序影响:它们用 `before`/`after` 名字向协调器 `register()`,而不是在 `ctx.on` 上抢位置,所以它们从不为 prepend 的位置竞争。在 dsh profile 里,把 `hooks-ordering` 那一行放进**用户的 `cordis.patch.yml`** 即可,它会在所有 bundle 层之后应用——于是该插件在构造上就是最后加载的。
228
+ - **DAG 日志写失败不会抛错**,只经 `console.warn` 报告:它跑在注册 effect 与 disposer 里,在那里抛异常会破坏 fiber 的拆卸。
229
+
230
+ ### 背景:今天在 dsh 里是怎么排序的(以及每种方式的局限)
231
+
232
+ 以下是 `deepseek-harness` 中现有的影响钩子顺序的办法——也就是本插件所取代的那些「绕行方案」。每一种都真实存在,也每一种都达不到声明式相对顺序:
233
+
234
+ 1. **在 `ctx.on(...)` 上用 `{ prepend: true }`**——引擎唯一的放置手段(`unshift` 对 `push`)。它是二元的,并且是*后 prepend 者覆盖先 prepend 者*:两个都 prepend 的插件会互相竞争,而谁都没法说「在前排之中排第一」。
235
+ 2. **注册 / `ctx.plugin(...)` 的调用顺序**——默认追加使得调用顺序就是执行顺序。这只有在某一个组装点控制所有调用时才有效,一旦互不相关的厂商以没人掌握的顺序加载就失效。
236
+ 3. **`inject` 依赖**——用服务可用性来把插件的*激活*卡住。这是把插件相对*服务*排序,**全局且单向**,并且强制一条依赖边。它无法表达已经挂在同一个钩子上的两个监听器之间的相对顺序,也无法表达不同钩子上的相反顺序。
237
+ 4. **顺序不变量断言**——能在事后发现错误的顺序,但并不会强制一个顺序。
238
+ 5. **Profile `cordis.patch.yml` 的行顺序**——明确**不**携带加载语义(activation is service-availability driven),所以它根本无法给监听器排序。
239
+
240
+ `HookOrdering`/`SerialHookOrdering` 用单个声明式原语取代以上五种:声明你的 `before`/`after`,注册到协调器,顺序就与加载时机无关地稳定下来——而且出问题时,约束图以 JSON DAG 的形式随时可查。
241
+
242
+ ## 开发
243
+
244
+ ```sh
245
+ pnpm install
246
+ pnpm test # vitest,单元测试 + 真实 cordis 集成测试
247
+ pnpm test:coverage # 逐文件 100% 门槛
248
+ pnpm typecheck
249
+ pnpm lint
250
+ pnpm build # tsdown -> lib/(ESM + d.ts + sourcemaps)
251
+ ```
252
+
253
+ 安装会走公共 npm registry(见 `.npmrc`),因为本包发布在那里。
254
+
255
+ ## 许可
256
+
257
+ [MIT](./LICENSE)