Skip to content

🐛 fix(sdk): hooks/skills 域十项修复——写回竞态、输入源错误转发、hook 隔离等 - #430

Merged
CavinHuang merged 15 commits into
mainfrom
fix/sdk-hooks-skills-misc
Aug 22, 2026
Merged

🐛 fix(sdk): hooks/skills 域十项修复——写回竞态、输入源错误转发、hook 隔离等#430
CavinHuang merged 15 commits into
mainfrom
fix/sdk-hooks-skills-misc

Conversation

@CavinHuang

Copy link
Copy Markdown
Owner

概述

修复 sdk 全量 review 提出的 10 个 issue(hooks / skills / 工具杂项域),每个 commit 独立可跑绿。

Issue 修复
#356 (P2) QueryController 输入源抛错经 AsyncInputQueue.fail 转发为迭代器错误进消息流,unhandledRejection 归零(有计数断言)
#348 skill evolution 写回段接入 withFileMutationLock + rename 前快照复核(外部修改即中止报错),restore 备份失败不再吞错继续覆盖
#382 hook matcher 正则编译失败隔离:日志+跳过该 def+上报 error 型 hook_response,其余 hook 继续执行
#355 hook 超时 timer 补 finally 清理(成功路径不再泄漏句柄);崩溃论断经实证为误报,语义未动
#385 NotebookEdit 结果不再内嵌两份 notebook 全文(全仓零消费已 grep 实证),只留变更 cell 摘要
#369 NotebookEdit insert 缺省锚点时插入 notebook 开头(与文档承诺一致;显式锚点与 replace/delete 行为不变)
#370 GNU grep 兜底引擎补齐 -A/-B/-C 上下文参数映射
#378 asset frontmatter 标题转义 \n\r\t 与 C0 控制字符(码点循环,代理对安全)
#349 skill getPrompt 参数改函数形式 replaceAll 字面展开,$&/$$ 等替换模式序列不再篡改 prompt
#383 注入 provider 后不再发射 auth_status 死事件(apps 全域零消费);rebase 时与 main 的 provider 移除(bf8b87671/6caa9fb0c)对齐做减法

验证

  • bun run typecheck 六 workspace 全绿
  • 改动模块单测 78 pass / 0 fail;agent-sdk 全量 583 tests / 0 fail;sidecar 下游 skill-evolution-service 18 pass
  • 独立 code review 通过:逐 commit 对照 issue + 全仓消费方 grep 实证(getApiType 保留依据、original_file/updated_file 零消费均复核成立)

Notes

rebase 至 main 最新并与 provider 移除语义对齐:#383 的 env 链清理部分被 main 等价实现吸收,本分支保留的行为增量为 auth_status 死事件移除与测试翻转。

Closes #348, closes #349, closes #355, closes #356, closes #369, closes #370, closes #378, closes #382, closes #383, closes #385

🤖 Generated with Claude Code

TaTaLiao and others added 9 commits August 22, 2026 17:40
replaceAll 字符串替换会把参数中的 $&/$$ 等序列当作替换模式解释,
改用函数形式保证占位符展开为原始字面量。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yamlString 此前只转义反斜杠与双引号,标题含裸换行或 C0 控制字符时
会产生非法 YAML frontmatter。补齐 \n/\r/\t 及其余控制字符的
\xNN 转义,保证 frontmatter 恒为合法单行。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildGrepArgs 未映射上下文行参数,rg 缺失回退 grep 时上下文请求被
静默丢弃。GNU/BSD grep 原生支持这三个参数,直接透传;注释标注
entries 计数口径与 rg/native 引擎的差异(上下文行计入输出行)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
findCellIndex 对缺省 cell_id 返回 0,insert 实际插到第一个单元格
之后,与文档 at the beginning 不符。insert 分支单独解析锚点:
cell_id 与 cell_number 均缺省时取 -1 走既有的插到开头分支;
显式锚点与 replace/delete 行为不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
original_file/updated_file 把完整 notebook JSON 塞进工具结果,
白白膨胀事件流与 token。结果只保留变更单元格摘要,全文按需 Read。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
两处修复:
- 函数 hook 超时的 setTimeout 从不清理,成功路径泄漏最长 30s 句柄;
  改为 race 结束即 clearTimeout,并为 handler promise 挂防御性
  noop catch,超时后的迟到 rejection 不外溢。
- matcher 正则编译在 try 块外,非法 pattern 同步抛穿循环被上层空
  catch 吞掉,该事件下全部 hook 静默丢弃;编译移入独立包裹,失败
  记日志、跳过该定义并上报 error 型 hook_response,其余 hook 继续。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
构造器对 streamInput 只挂 finally 无 catch:async iterable 输入源
中途抛错即 unhandledRejection,且队列永不关闭导致 runner 永久
pending。AsyncInputQueue 增加 fail(),构造器补 catch 后经其把错误
转发为迭代器错误进入消息流;push-after-close 同链路一并覆盖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
applySkillImprovement 的 readFile→备份→模型长窗口→rename 无条件覆盖
链路无锁无复核,外部修改会被静默覆盖。整个写回段接入
withFileMutationLock 串行化,rename 前重读快照比对,内容变化即清理
备份并中止报错。restoreSkillVersion 备份当前内容失败时不再吞掉错误
继续覆盖,改为中止恢复。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
宿主注入 provider 契约下 SDK 不持有凭据,auth_status 在 apps 全域
零消费,每轮 run 纯属死重:入口 fail-fast 已保证到达运行循环必有
provider,直接删除该事件发射并同步调整断言其存在的测试。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CavinHuang

Copy link
Copy Markdown
Owner Author

Code Review

逐项核对了十项声称的修复,全部真实落地,修复方向无一修错,CI 6/6 绿(含长期红的 Computer Use 本轮也绿),可合并。以下为遗留发现。

逐项核对结论

Issue 结论
#356 输入源错误转发 ✅ 两半都处理:AsyncInputQueue.fail() + 构造器 .catch;push-after-close 的 rejection 也被吞掉不再 unhandled
#348 写回竞态 ✅ 按字面闭环:全程持锁 + rename 前快照复核(fail-closed)+ restore 备份失败中止;但见发现 1
#382 matcher 隔离 ✅ try/catch + error 型 hook_response + continue;但见发现 4
#355 timer 泄漏 ✅ try/finally 清理;「崩溃论断为误报」的更正经核实正确
#385 notebook 结果减肥 ✅ original_file/updated_file 全仓零消费实证成立,无白读残留
#369 insert 缺省锚点 ✅ 显式锚点行为不变,有测试钉住
#370 grep -A/-B/-C ✅ 三引擎对齐;total_matches 口径偏差已在注释自我披露
#378 YAML 标题转义 ✅ 码点循环代理对安全,C0/DEL 全覆盖
#349 getPrompt 字面展开 ✅ 函数形式 replaceAll 正确,测试含 $&/$$/$'/$`
#383 auth_status 死事件 ✅ 零消费实证成立;entry check fail-fast 有新测试

发现

[P2] restoreSkillVersion 未接入 withFileMutationLock,apply 与 restore 并发仍可互相覆盖

  • 位置:packages/sdk/src/skills/evolution.ts:208(对比 :125 applySkillImprovement 已全程持锁)
  • 锁基建引入后只保护了 apply 一条写路径。restore 有自己的 read→backup→rename 窗口却无锁,两个操作并发时序:apply 快照复核通过 → restore rename 覆盖 skillPath → apply rename 再覆盖 → restore 报成功但效果丢失。sidecar 的 skill-evolution-service.ts 中 apply 与 restore 是并列 RPC 入口,可并发到达。双方均有版本备份兜底所以数据不丢,但操作语义交错——file-mutation-lock 的语义是 "Serialize mutations",restore 显然也是 mutation。
  • 建议:restore 的 backup→write→rename 段同样包进 withFileMutationLock(input.skillPath, ...),一处改动即可,建议作为紧随的 follow-up。

[P3] NotebookEdit insert 对正超界 cell_number 静默追加尾部,与 replace/delete 的报错不一致packages/sdk/src/tools/notebook-edit.ts
replace/delete 分支有 targetIndex >= cells.length 守卫报 "Cell does not exist",insert 分支对 cell_number=100、cells.length=2 会因 splice clamp 静默追加尾部。属既有行为非本 PR 引入,但本 PR 触碰的正是该行并声称处理了边界。建议补对称校验。

[P3] 「排队等待既有文件锁」测试无法钉住锁语义packages/sdk/src/skills/evolution.test.ts
withFileMutationLock 实现退化(立即执行回调),此测试仍全绿——只能捕获嵌套死锁,捕获不了锁失效回归。建议在 callModel 回调内断言外部持有者尚未释放。

[P3] matcher 隔离两个小缺口packages/sdk/src/hooks.ts
(a) 编译发生在 if (def.matcher && input.toolName) 内,SessionStart/Stop 等无 toolName 事件下坏 matcher 被静默放行并照常执行 handler,隔离不完整(与 main 一致非回归);(b) 每个 PreToolUse/PostToolUse 事件重复编译失败 + console.error 一次,高频下日志噪音持续。建议注册期预编译并告警,执行期用缓存。

[P3] applySkillImprovement 持锁跨 callModel 且锁无超时护栏
callModel 由宿主注入,若挂死则同 key 的所有后续 mutation(包括 agent 的 Edit/Write 工具,共用同一把锁)无限期排队。建议给锁等待加可选超时,或在文档标注「operation 必须自证有界」。

[P3] #383 残留:engine.ts 的 CODEANY_API_KEY env 回退链仍在packages/sdk/src/engine.ts
apiKeySource 已被 main 硬编码 'configured',这行读全仓无赋值方的 env 永远走 'unknown'。PR 描述已如实说明部分被 main 吸收,此项为剩余残留记录,后续清理批次顺手删即可。

@CavinHuang
CavinHuang merged commit c431d4c into main Aug 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment