Skip to content

feat(request): 恢复被丢弃的推理签名并在工具循环中回放 - #79

Merged
sunerpy merged 9 commits into
mainfrom
feat/reasoning-signature-replay
Jul 30, 2026
Merged

feat(request): 恢复被丢弃的推理签名并在工具循环中回放#79
sunerpy merged 9 commits into
mainfrom
feat/reasoning-signature-replay

Conversation

@sunerpy

@sunerpy sunerpy commented Jul 30, 2026

Copy link
Copy Markdown
Owner

问题

Kiro 在流式响应中会返回一段带有不透明 signature 的原生推理内容(reasoningContent.reasoningText.{text, signature})。插件此前只渲染了其中的 text 用于展示,signature 直接丢弃了

后果是:在多轮工具循环里,历史消息中不再携带可被上游校验的推理签名,模型无法延续上一轮的思考链,推理质量随轮次逐步退化——到后几轮甚至完全不再产生推理帧。

修复

  • 捕获签名:流式转换层完整解析 Kiro 原生推理事件,保留 signatureredactedContent,不再丢弃。
  • 关联缓存:以「本轮实际发出的推理内容」作为指纹,把签名放入进程内有界缓存(64 条总量 / 单轮 16 条 / TTL 30 分钟),避免无限增长与跨会话污染。
  • 历史重建:构造下一轮请求时,按精确指纹匹配重建嵌套结构 assistantResponseMessage.reasoningContent.reasoningText.{text, signature},把签名回放给上游。
  • 签名被拒的恢复路径:上游返回 400 THINKING_SIGNATURE_INVALID 时,剥离已回放的历史推理内容,在同一个已固定的账号上重试一次,避免账号轮换引入新的不一致。
  • 日志脱敏:日志与错误信息中的推理签名统一去敏,不再明文落盘。

设计约束(有意为之)

  • 纯数据驱动,没有模型白名单:只依据实际收到的事件结构决定是否回放,不维护「哪些模型支持推理」的名单。
  • 任何歧义 ⇒ 不回放 ⇒ 零回归:指纹不匹配、缓存缺失、结构不完整——任一情形都退回到修复前的行为(不回放),因此本改动不会让任何原本可用的路径变差。
  • 携带原生推理的回合不得同时携带 <thinking>:两种表达互斥,避免上游收到重复/冲突的思考内容。

验证证据

真机 A/B 对比(提交在 scripts/probes/AB-OPENCODE-COMPARISON.md,24 次真实 OpenCode 运行):

分组 结果
回放签名 5 轮全部保留推理,任务正常完成
不回放(修复前行为) 第 3 轮推理帧塌缩为 0,任务被放弃

相关证据与脚本:

  • scripts/probes/AB-OPENCODE-COMPARISON.md — 端到端 A/B 对比
  • scripts/probes/ab-reasoning-probe.tsscripts/probes/*/results/ — 原始数据
  • scripts/probes/README.md — probe 使用说明

明确的范围边界

本 PR 不修复「模型提前结束回合」的问题。那是一个完全独立的缺陷(工具结果回合的 content 填充文本被模型当成真实用户发言),在另一个 PR 中单独修复。请不要把两者的效果互相归因:签名回放不解决提前结束,提前结束的修复也不影响推理连续性。

本 PR 同样不涉及另有记录的「HTTP 200 但响应体完全为空」的失败模式。

sunerpy added 9 commits July 29, 2026 23:07
package-lock.json 锁定 @aws/codewhisperer-streaming-client@1.0.39,而 bun.lock
与 node_modules 已是 1.0.45。CI 和发布流程均只用 bun install,npm 仅用于
npm publish(不读锁文件),因此该文件不参与任何流程,仅会误导执行 npm ci 的
贡献者装到过旧的依赖树。

- git rm package-lock.json
- .gitignore 新增 package-lock.json,避免误跑 npm install 后静默复活
- README.md / docs/readme/README.zh.md 在既有开发小节内说明 Bun 是唯一
  受支持的开发包管理器
@ai-sdk/openai-compatible 把助手推理序列化为顶层 reasoning_content 字符串、
content 保持普通字符串,而 buildHistory 只在 content 为数组且含 type:'thinking'
时才走 thinking 分支,否则回落到只筛 type:'text' 的 getContentText。结果是
OpenCode 路径上的推理文本每轮都被静默丢弃,原有的 <thinking> 重建实为死代码。

- message-transformer.ts 新增共享解析器 parseAssistantMessage,以及
  extractReasoningText / findActiveToolLoopStart / applyThinkingToContent
- history-builder.ts 与 request.ts 的两处重复助手分支改为调用共享解析器,
  各由约 30 行缩至 3 行
- 恢复范围限定在进行中的工具循环,叠加未改动的 collapseAgenticLoops,
  上线的 <thinking> 块恒定不超过 2 个(实测 +211 est. tokens,真实会话约 +1.8~2.8%)
- 恢复文本只进 assistantResponseMessage.content;无签名的 reasoningContent
  会被服务端硬拒 400,故此处不写该字段
- 新增 reasoning-text-recovery.test.ts(22 例)覆盖字符串 content 携带
  reasoning_content、数组 thinking 回归、工具循环边界
transformSdkStream 此前以 `event.reasoningContentEvent?.text` 为门,导致只带
signature 或只带 redactedContent 的事件穿过所有分支后消失。Kiro 每个响应尾部
恰有一个这样的事件,携带整段推理的签名,因此签名从未被捕获。

- types.ts 新增 KiroReasoningContent(text 变体的 signature 为必填:无签名的
  reasoning 块会被服务端硬拒 400),并为 assistantResponseMessage 加上可选的
  嵌套 reasoningContent 字段
- 新增 ReasoningAccumulator:逐字追加文本、接受纯签名事件、保留最后一个非空
  签名、两个不同的非空签名判为不支持而非猜测、redacted 字节按 SDK 类型复制
  且不经文本或 base64 往返
- 门改为按 reasoningContentEvent 存在性分派,内部再按字段派发;纯签名事件不
  触碰 reasoningStarted/reasoningClosed/thinkingBlockIndex,可见输出逐字节不变
- 诊断日志只含种类与长度,签名与 redacted 字节永不落日志
- 新增 reasoning-accumulator.test.ts(21 例),含可见输出字节一致性对照与
  finalize 后修改源数组证明字节为复制而非别名

本波仅做观测,不改变任何可见行为;发布时机属 Wave 4。
Kiro 会校验历史里回放的每个推理签名,压缩历史、跨模型回放或签名过期都会得到
一个流开始之前的 400 THINKING_SIGNATURE_INVALID。这是请求级错误,正确处理是
剥离全部历史推理后原账号重试一次,而不是换账号或标记账号不健康。

三处管道此前都不通:
- request-handler.ts 的合成错误体只带 message/__type,SDK 建模的 reason 字段
  被丢弃,导致 errorData?.reason 恒为 undefined(而 402/403 分支正依赖它)
- error-handler.ts 的 400 分支无条件返回 shouldRetry: false,从不看 reason
- RequestContext 只有 retry 与 forcedRefreshAccountIds,而 prepareSdkRequest
  每次重试都从原始 body 重建,无处记录"本次已尝试恢复"

改动:
- 合成错误体条件性附加 reason,无 reason 的错误产出与此前逐字节相同
- 400 分支先判定 size-overflow 并作为恢复分支的守卫,确保 413 重映射路径不被
  拦截;匹配 reason 精确相等,不做文本模糊匹配
- RequestContext 新增 signatureRecoveryAttempted(限一次,模仿
  forcedRefreshAccountIds 的收敛方式)与 disableReasoningReplay
- 新增 ErrorHandlerResult.pinAccount,复用既有 forcedStreamAccount 机制钉住
  同一账号,不消耗轮换额度,不触碰健康状态
- transformToSdkRequest 第 7 参改为 SdkRequestOptions,置位时调用新增的
  stripReasoningContent 清除 history 与 currentMessage 上的 reasoningContent
- 新增 signature-recovery.test.ts(20 例),含同时带 overflow 文案与签名 reason
  的对抗用例、重复被拒的收敛用例,以及穿过真实模块边界断言标志确实抵达
  transformToSdkRequest 的用例
- 新增 EmittedOutputAccumulator,从发出的 OpenAI 分块累积指纹来源
- 新增 loopId 派生与长度前缀指纹键,模型作为命名空间隔离
- 新增有界关联缓存,读取非消耗,歧义与近似一律拒绝
- onComplete 扩展为完整载荷,三个完成路径共用同一发布入口
- 新增 log-redaction 模块,将签名与 redacted 字节收敛为
  {present,length|byteLength,sha256Prefix}
- 去敏落在两个日志 sink 内部而非调用点,任何调用方无法再通过打印
  prepared request 或 conversationState 泄露签名
- 抽出 sdk-log-payload,出站请求日志改为携带按轮次的去敏推理摘要
- 环形结构改为抛出与 JSON.stringify 相同的 TypeError,保持原有
  不可序列化行为不变
- 新增 14 条测试,覆盖两个 sink、出站载荷、抛出的错误与共享子对象
- 提交去敏后的 A/B probe 与 README,凭据运行时读取、邮箱与 ARN 掩码、
  写盘前自检,并记录原始两轮结果 JSON 已随 /tmp 丢失
- 提交 A/B 探针的真机原始结果 JSON,作为 §3.1 在 Wave 5 之后的可复现基线
- 新增跨账号回放探针:双账号必须显式指定,任一额度不足或令牌将过期即拒绝
- 新增插件级轮换测试:真实 AccountManager 轮换后出站体仍携带原账号的字节级签名
- README 记录哪些原始结果是权威的、为何不提交 dry 结果、以及探针与插件的验证边界
- 新增 .prettierignore 保护原始证据不被重新格式化
在真实 OpenCode 二进制上,用同一条长依赖工具链、同一个 Kiro 模型与同一份
fixture,对比已发布的修复前插件 0.15.4(== 80782f9)与本地工作树 HEAD,
唯一变量是加载了哪个 kiro-auth 构建。不涉及发版,未改动任何生产代码或既有测试。

- build-fixture.ts 幂等生成两套 fixture:baseline 10 跳(FINAL_TOTAL=231)与
  stress 20 跳 + 每轮模数校验和(459 / 8553),遍历顺序刻意非顺序,模型无法
  预测下一跳文件名,因此提前停止可被客观计数
- run-ab.ts 交替执行两臂并落盘完整 --format json 事件流、opencode export
  会话(含 reasoning 部件)与插件出站 api 日志;analyze-ab.ts 从原始产物重算
  全部表格;sanitize-runs.ts 产出脱敏可提交副本
- 隔离机制实证:OPENCODE_CONFIG 与全局配置【合并】而非替换,无法用于隔离插件;
  OPENCODE_CONFIG_DIR 在 1.18.5 上无可观察效果;只有 XDG_CONFIG_HOME 能整体
  重定位配置解析。插件的 kiro.db 与刷新锁同样挂在该变量下,故符号链接回真实
  目录(拷贝会因 refresh token 一次性而废掉真实账号)
- 臂身份用 4 格置换对照证明:两构建的 package.json 版本号与 dist/index.js 哈希
  完全相同,只有隐藏对侧 dist 后观察 kiro 模型是否消失才能区分
- 结果(24 次真机运行,486 次真实调用):机制确认端到端生效(NEW 臂最多
  18/21 个请求携带 historyReasoning、66 个签名信封;OLD 臂结构上为 0),
  未命中即无操作;但“宣告下一步后停止”在两臂都出现(10 跳 2/8 vs 1/8,
  Fisher p=1.0;20 跳两臂各 0/4),且三次停止全在第 2 轮、此前推理量为 0,
  故该症状不由推理坍缩解释
- AB-OPENCODE-COMPARISON.md 为独立可读的对比报告,含复现命令、作废批次记账、
  残留混淆项与诚实裁决
@sunerpy
sunerpy merged commit 5d45121 into main Jul 30, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 30, 2026
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/core/request/error-handler.ts 99.23% <ø> (+0.10%) ⬆️
src/core/request/request-handler.ts 99.32% <ø> (+2.02%) ⬆️
src/core/request/response-handler.ts 99.51% <ø> (+0.03%) ⬆️
src/core/request/sdk-log-payload.ts 100.00% <ø> (ø)
src/infrastructure/transformers/history-builder.ts 90.76% <ø> (+5.34%) ⬆️
...infrastructure/transformers/message-transformer.ts 100.00% <ø> (ø)
src/plugin/log-redaction.ts 97.61% <ø> (ø)
src/plugin/logger.ts 98.76% <ø> (+<0.01%) ⬆️
src/plugin/reasoning/correlation-cache.ts 100.00% <ø> (ø)
src/plugin/reasoning/emitted-output.ts 100.00% <ø> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant