4.8 KiB
Agent Note: 收紧 hook-protocol 约定——dialect、被丢弃的字段、双重默认值与 lib 拥有的 hook/result 语义
Status: implemented
English | 中文
问题
dsh-hook-protocol/bridge 约定中有四部分没有遵守 subagent observe/enrich Agent Note 记下的准则——后者因缺少消费方而删除 agentType 生命周期字段,以下各项没有通过同一检验:
HookDialect的'native'变体(packages/hooks/hook-protocol/src/types.ts)没有生产者——bridge 会标记'claude'和'codex';所有位置中唯一构造'native'的是该库自己的单元测试。字段自身的 JSDoc 将dialect定义为「运行它的 bridge」,而 native 不是 bridge:拦截扩展点 Agent Note 记载 native 钩子不是一个包,并且「native 插件无需持久钩子日志即可使用类型化 Decision」;旗舰 native 插件实践示例恰好断言了这一点(完全没有hook/*事件)。HookOutput.suppressOutput(同一文件)被 codec 解析后在所有路径上均被丢弃:没有 bridge 分支处理它、没有合并 fold、没有 warn、没有 deferred-list 行——在所有「被解析但未兑现」的同类字段中它是唯一没有明确延期声明的(updatedInput→ 一条 warn 日志加 pre-tool-input-rewrite 提案;systemMessage→ 一条 warn 日志加 README deferred 行;continue/stopReason→ 一个TODO(hook-continue-false)锚点加'stop'decision 记录)。从结构上看根本无物可抑制:钩子 stdout 从不进入任何 transcript(文本记录);上下文仅通过additionalContext流入,日志也只记录decision/stderrSummary。因此,钩子作者设置suppressOutput: true得到的是无声的空操作,且无任何警告。defaultTimeoutMs在两个 bridge 配置中都以游离的字面量重复设置了默认值——schema 的.default(600_000)加上一个?? 600_000回退(packages/hooks/hooks-claude-code/src/index.ts、packages/hooks/hooks-codex/src/index.ts),一个协议级常量在每个 bridge 中有两个归属地,两个 bridge 可能在共享默认值上悄然分歧。按 no-hardcoded-tunables 规则,该旋钮保留为 bridge 拥有的显式配置(旁边有stderrSummaryMaxChars);要修的是字面量的归属地。hook/result的语义存在于两个 bridge 中(各一份),而非拥有该事件的 lib。summarize()——stderr 截断规则——在packages/hooks/hooks-claude-code/src/index.ts与packages/hooks/hooks-codex/src/index.ts中逐字节相同;decision 字符串规则output.decision ?? (output.continue === false ? 'stop' : 'pass')同样如此。然而dsh-hook-protocol声明了hook/result、在文档中将stderrSummary描述为「已截断」却不拥有截断逻辑,记录了 decision 值却不拥有映射逻辑。如果某个 bridge 漂移(不同的上限、不同的回退),共享持久化事件的语义就会悄然分叉。
决策
HookDialect 是封闭的 bridge 集合:'claude' | 'codex';HookOutput 移除了不受支持的 suppressOutput。hook/result.durationMs 保留为持久化的审计计时,仅在快照中做归一化。参考默认值各只存在一处:DEFAULT_HOOK_TIMEOUT_MS 与 DEFAULT_STDERR_SUMMARY_MAX_CHARS。HookResultRecord 与 appendHookResult 共同负责两个 bridge 的 stderr 摘要化和 decision 推导逻辑。BLOCKING_EXIT_CODE 为 codec 内部常量。
曾考虑的替代方案
为什么不保留它们?
不受支持的词汇可以在真正有消费方时回归。durationMs 保留,因为持久化的审计计时独立于当前是否有读取方而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享持久化事件的归一化属于协议库。
验证
HookDialect 仅包含 Claude 和 Codex,suppressOutput 在源码、已解析字段文档和归一化逻辑中均不存在。durationMs 保留在事件和 fixture(测试前置数据)中,回放时做清洗。600_000 和 500 两个默认值各只在协议库中出现一次;每个钩子的超时覆盖仍然生效;两个 bridge 的测试套件均验证了由库拥有的 stderr 截断和 decision 规则。
后果
dialect、suppressOutput、可调参数和语义变更在协议格式(wire format)和预期输出中均不可见。代价是 dsh-hook-protocol 和两个 bridge 中的改动——在预发布立场下成本很低,也比让一项持久事件语义的两个副本各自老化更便宜。