Skip to content

🐛 fix(sdk): utils 域八项修复——结构化输出兜底、Retry-After 退避、审批大小写等 - #427

Merged
CavinHuang merged 21 commits into
mainfrom
fix/sdk-utils-prompt
Aug 22, 2026
Merged

🐛 fix(sdk): utils 域八项修复——结构化输出兜底、Retry-After 退避、审批大小写等#427
CavinHuang merged 21 commits into
mainfrom
fix/sdk-utils-prompt

Conversation

@CavinHuang

Copy link
Copy Markdown
Owner

概述

修复 sdk 全量 review 提出的 8 个 issue(utils / prompt / token 域),每个 commit 独立可跑绿。

Issue 修复
#318 (P1) structured-output 解析失败后兜底提取首个平衡 JSON 块(跳过字符串字面量与转义),重试提示强化首字符必须是 {
#291 settings user 路径遵循 LUME_CONFIG_DIR(与 fs-loader 同款语义:trim/空白回退/相对路径基于 cwd)
#350 markdown-frontmatter 放行顶格 - 列表项,items 为空时告警(skills allowedTools 不再静默丢失)
#351 重试退避尊重 Retry-After(delta-seconds/HTTP-date 双格式解析,120s 硬上限);挂载点迁移见 #422
#354 settings 读取区分 ENOENT(静默)与坏 JSON/权限(console.warn 带路径),安全配置不再 fail-open 无声
#364 microCompact 补数组形态 tool_result:text 块按预算截断;仅超预算的 image/document 块替换占位(小图原样保留,不损伤视觉能力)
#366 getContextWindowSize 硬编码表未命中回退 findModelMeta(model)?.contextWindow(gemini/qwen 1M 窗口不再落 200k)
#379 工具审批通配符与精确匹配改大小写不敏感(deny 不再漏配 fail-open;mcp__ 前缀结构保留不过归一)

验证

Notes

Closes #318, closes #350, closes #351, closes #354, closes #364, closes #366, closes #379, closes #291

🤖 Generated with Claude Code

TaTaLiao and others added 10 commits August 22, 2026 17:12
模型输出前带说明文字时,锚定围栏正则剥不掉前缀,JSON.parse 直接失败,
重试耗尽后整 run 误判 error_max_structured_output_retries。

- parseStructuredOutput 失败后新增首个平衡 {} 块提取兜底,
  正确跳过字符串字面量与转义序列
- 重试提示强化:首字符必须是 {,禁止前置散文/围栏/注释

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
标准 YAML 允许列表项不缩进,旧实现强制要求缩进导致顶格项被静默丢弃,
key 落空串(skills 的 allowedTools 等字段丢失)。

- 终止条件收敛为:遇到非 "- " 形状行(新 key 或其他内容)或 "---"
- 顶格与缩进列表项均接受
- key 值为空且未收集到任何列表项时输出告警

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getRetryDelay 纯指数退避,两个 OpenAI provider 构造错误对象时丢弃
Retry-After 头,429 限流时按固定节奏盲重试。

- 新增 parseRetryAfterHeader:支持 delta-seconds 与 HTTP-date 两种格式
- 错误对象挂 err.retryAfterMs,withRetry 经 computeRetryDelay 优先采用,
  无抖动,硬上限 120s;未携带时回退指数退避
- openai / openai-responses 构造错误时解析并挂载该头

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
microCompact 只截断 string 形态 tool_result,数组形态(读图 base64、
web-fetch 结果)原样进 provider,预算形同虚设。

- 数组分支 text 块按预算首尾截断,与 string 分支同一标记
- image / document 块整体替换为占位 text 块,注明原块类型与原始大小
- 无变化时保持原引用,避免无谓重建

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
窗口表纯硬编码名称子串,gemini/qwen 等 1M 窗口模型落默认 200k;
findModelMeta 已 import 但只用于价格。

- 表未命中后回退 findModelMeta(model)?.contextWindow,再落默认值
- 已知模型的表内结果保持不变

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
通配符规则提前 return 绕过别名归一且前缀比较大小写敏感,deny 规则
大小写不一致时漏配 fail-open;MCP 未知名精确匹配同样受影响。

- 通配符分支改为原始名小写前缀比较,不对前缀套别名归一
  (归一会剥坏 mcp__ 结构)
- 别名比对两侧统一小写,未知名精确匹配随之大小写不敏感

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
settings 的 user 分支硬编码 ~/.lume,不读 LUME_CONFIG_DIR,与同包
fs-loader/worktree-tools 及 desktop/sidecar 行为不一致,自定义配置目录
下 user 级 settings.json 永远读不到。

- 按 fs-loader 同款逻辑解析 env(空白回退默认、相对路径基于 cwd 解析)
- 未设置时行为不变

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
catch 把 ENOENT 与 JSON.parse 失败同等静默吞掉,安全相关配置损坏时
fail-open 无任何痕迹。

- 非 ENOENT(坏 JSON、权限等)console.warn 输出文件路径与错误信息
- 缺失文件维持静默跳过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
review 发现 34a35dbfe 对数组 tool_result 里 image/document 块一律替换
占位,而 microCompact 每轮对全量消息执行,小图也被剥掉,SDK 直接消费者
的图片/PDF 视觉能力整体失效。

- 媒体块加尺寸门槛:序列化大小超过 maxToolResultChars 预算才替换为
  占位 text 块(注明类型与原始大小),小图原样保留
- 测试同步修正:超大 image/document 占位、小 image 深比较原样保留

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linux CI 上 join("D:", ...) 非绝对路径,被 resolve 正确地拼到 cwd 后,
断言失败;实现行为无误,属测试的平台假设错误。

- win32 取 D:\custom-lume,POSIX 取 /tmp/custom-lume
- 两侧均断言 resolve 后的精确路径

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

Copy link
Copy Markdown
Owner Author

Code Review

八项逐项核对完毕,全部真实落地、无虚报,无 P0/P1,可合。以下为修复深度/跨层一致性的遗留发现。

逐项结论

#318(兜底+提示强化)、#350(放行顶格列表+空值告警)、#354(ENOENT 静默/其余 warn 带路径,判定正确)、#364(超预算才占位的设计保住了视觉能力)、#366(registry 回退已验证 gemini-2.5-pro=1048576 且 bundled seed 使测试非空洞)、#379#291(与 resolveConfigDirValue 语义逐点对齐)、#351(非流式路径真实生效;流式主链路由 sidecar 既有逻辑承担,披露诚实)。#379 的实现方向(raw lowercase 前缀而非过别名归一)比 issue 原处方更正确。

发现

[P2] #318 兜底只尝试首个 {,prose 中更早的非 JSON 花括号对会让兜底彻底失效structured-output.ts:63-67
兜底只提取第一个平衡 {...} 块;若该块 parse 失败整个解析直接放弃。模型输出 The config {mode} controls it.\n{"answer":"ok"} 时 candidate={mode} 解析失败 → 重试耗尽——这正是 #318 要救的场景的残余形态(prose 含花括号极常见)。新测试只钉 happy path,无一覆盖前导花括号噪音。建议 candidate 失败后从下一个 { 循环继续提取。

[P2] #364 数组形态预算逐块生效而非逐结果,多中块 tool_result 聚合仍不设防compact.ts:738-764
text/image/document 块均按单块阈值判定,无 per-result 累计。50 个 49k 文本块或 20 个 40k 小图(合计 1-2MB)每块都低于门槛整体原样通过。microCompact 作为 prompt-too-long 前的防御层,聚合洞恰是它存在的意义。建议函数内累计放行块总长,超预算后剩余块统一占位。

[P2] #351 与 sidecar 形成两套 Retry-After 语义,sdk 侧在 Retry-After: 0 时退化为零延迟风暴retry.ts:374-388 vs pi-ai-provider.ts:29,78-88
sdk 侧有 retryAfterMs完全取代指数退避(无 jitter 无保底,Retry-After: 0 → 0ms 立即重试);sidecar 侧取 max(jittered, retryAfter) 保底;硬上限 120s vs 30s 分叉。生产主链路重试发生在 sidecar 内部循环,同一错误先经 sidecar max 语义重试 5 次耗尽后才进 sdk replace 语义再试。建议至少统一硬上限,sdk 改用 max(退避, hint),并在 #422 明确两层策略契约。

[P3] #366 硬编码 includes 表仍遮蔽 registry 较新数据tokens.ts:163-166):model.includes('sonnet-4') return 200_000 抢先于 registry 回退命中,而 generated.json 中 claude-sonnet-4-5/4-6/5 均为 1M——「表未命中」修了,「表命中但值过时」没修。生产 sidecar 显式传 contextWindow 掩盖。建议表项逐步收敛为 registry 优先。

[P3] #364 热路径对所有媒体块做完整 stringify 计长compact.ts:753 + engine.ts 每 turn 全量历史):hydrate 已把图片展开为完整 base64,单块可达数 MB,历史多截图时每轮重复付出。建议用 source.data.length + 常数开销 估算替代。

[P3] 新重试提示语对数组根 schema 是错误指引engine.ts:1506):"must begin with {",但 jsonSchema 是公开 SDK 选项且数组根合法,extractFirstBalancedJsonObject 也只认 {。仓内无数组根用法,风险面在外部 SDK 消费者。建议按 schema.type 动态生成或改措辞。

[P3] #350 空列表告警对合法 YAML null 值键误报markdown-frontmatter.ts:58-61):tags: 后紧跟下一个 key 是合法 YAML(null 值),现在每次加载都告警,skills 每会话加载噪音累积。建议仅对已知列表语义键告警。

[P3] #379 测试缺口:别名通配符场景(issue 的核心反例)未钉住tool-approval.test.ts:23-42):缺 matchesToolPattern("Bash", "bash*") 用例;若后续有人把 wildcard 分支改回经别名归一(会破坏 mcp__ 结构),现有测试部分不红。

CI 6/6 全绿、typecheck 绿。建议 #318 循环提取与 Retry-After 上限统一随本 PR 或紧跟 #422 处理;其余 P3 可入 follow-up。

@CavinHuang
CavinHuang merged commit 439305f 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