Skip to content

RFC-0048: 录制 schema 3 与传输回放 - #325

Merged
eric8810 merged 11 commits into
masterfrom
rfc/0048-recording-schema-3-and-transport-replay
Oct 25, 2026
Merged

eric8810 merged 11 commits into
masterfrom
rfc/0048-recording-schema-3-and-transport-replay

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Requested by li

Summary

本 RFC 是 RFC-0041 G1–G3 的落地设计,接替 #270:录制只写 schema 3,在传输叶子记录原始字节与分块;回放是一个 Fetch,离线组装的 provider 照常编码和解析,每个 provider 都能离线回放;删除 schema 1/2 读取、ProviderRecord、MockReplayModel、rebuild_provider。crate 归属以 RFC-0044(#279)为准,本文不定。下面先是录制与回放子系统的现状,再是本 RFC 设计的改动。

录制与回放子系统现状

行号是 master b91829af。

录制
aimux_core::generate_text                          aimux-core/src/generate.rs:954
 └ recording::recorder()                           generate.rs:1079        读全局 RECORDER
   └ record_input / record_provider                generate.rs:1082 / :1088 ← provider_id 由 provider() 按 "." 截断猜出
 └ RecordingContext::scope                         generate.rs:1131        task-local
   └ model.do_generate → provider 编码 → HttpRequest::new
     └ RecordingContext::current()                 aimux-provider-utils/src/http.rs:126 ← provider-utils 读 core 的 task-local
     └ send_request_once → fetch.fetch             http.rs:305 / :375
     └ observe_response → record_exchange          http.rs:628 / :647      请求体存脱敏后的 JSON 文本
       └ ResponseRecording::finalize               http.rs:765             分块拼成 String、1 MiB 截断、from_utf8_lossy
 └ record_outcome                                  generate.rs:1206
 JsonlRecorder writer_loop → ready()               aimux-core/src/recording.rs:1177 / :185   完成屏障

模型层 mock 回放
aimux_ops replay.create → MockReplayModel::new     aimux-ops/src/ops/replay.rs:243 / :236
 └ MockReplayModel::do_generate                    aimux-core/src/replay.rs:519
   └ rebuild_generate_result                       replay.rs:652           只认 OpenAI chat,跳过 provider 解析器

live 回放
aimux_ops replay.live(registry)                   aimux-ops/src/ops/replay.rs:305
 └ rebuild_provider                                aimux-providers/src/replay.rs:58   按猜出的 provider_id 重建
 └ replay_with_model → generate_text_from_language_model_prompt
                                                    aimux-core/src/replay.rs:1029 / :1087

上游没有录制格式:@ai-sdk/devtools 在模型调用层用 middleware 和 telemetry 记步骤(devtools/src/middleware.ts:101、integration.ts:113);上游测试用 createTestServer 在 fetch 处喂响应(test-server/src/create-test-server.ts:112),provider 照常编码解析。

本 RFC 的改动

录制(建在 RFC-0044 C3 的 Fetch 观察点上,Q1 已定)
aimux::generate_text
 └ 调用观测接口:callId、operation、target{ modelRef?: RecordedModelRef, model }、Captured<input>   ← 接法由 RFC-0044 §4.1 的后续 RFC 定
   └ 每个模型调用 attempt:(seq, attempt, parent?)               ← RFC-0044 §4.2 DiagnosticScope
     └ provider 编码 → helper
       └ Fetch 观察点
         ├ ① 请求原始字节与 headers:按包登记的 ExchangePolicy 存值,authentication 与未登记的头只留名字
         ├ ② 叶子 Fetch
         ├ ③ 响应头 + responseHeadOffsetUs
         └ ④ 有序分块 {offsetUs, bytes},终态时整段做 RFC-0041 §5 资格判定 → eof / error{kind} / abandoned
           → aimux-devtools Recorder:schema 3、Captured 四态(not-applicable 优先)、完成屏障、JSONL / Ring

读取
ReplaySet::load(jsonl, limits)                                 schema ≠ 3 → UnsupportedRecordingSchema
 └ deny_unknown_fields + 结构校验                              complete=true 才要求每个交换有终态
 └ Recording::replayability(policy)                           重算 mock(传输回放)、exact:… 与 operation:… 原因

离线回放
aimux_ops replay.session { replay, callId, matcher? }         新 op
 └ ReplaySession::new(recording, Exact | Sequential, exchange_policy)
aimux_ops replay.offlineModel { session, seq?, provider? }    新 op,router / MoA 子调用各取一个
 └ create_offline_provider(name, session.fetch())             aimux-providers,占位 key;name 按 model.provider 精确查
aimux_ops replay.offline { session, model?, input? }          新 op,关闭 G-k
 └ replay_with_model / replay_decision_with_model
   └ provider 编码 → ReplayFetch::fetch                        永不联网
     ├ Exact:RFC-0041 §7 匹配视图(认证 query/头两边排除,JSON 无损比较,multipart 按 MIME)
     └ 吐出录下的 status、有值的 headers、分块与终态 → provider 解析器
 └ verify_consumed()                                          未消费的 model 交换 → ReplayUnusedExchange

live 回放
aimux_ops replay.live(registry)→ RecordedModelRef 解析回 ops ModelRef,按 namespace/key/method/modelId 取模型   删除 rebuild_provider

RFC 还写了:schema 3 的完整形状与读取规则(§3)、匹配与响应回放规则(§5)、离线组装与 ExchangePolicy 登记(§6)、ops 每个 op 的改动和错误码、绑定与工具(§7)、与 RFC-0041 / RFC-0044 的对照(§8)、本期不做的项与登记差异(§9)、实施计划与 schema 2 fixture 去向(§10),以及维护者已定的五个问题(§11)。

How

只新增 rfc/0048-recording-schema-3-and-transport-replay.md,不改代码。

🤖 Generated with Claude Code

https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

对照 RFC-0044(#279,6afa014a)核对了一遍。§4.1 的观察点位置、§8 表、§10 的顺序和 Q2 对 RFC-0044 §3.1 的转述都和 #279 一致。有三处需要对齐:

位置 RFC-0048 现在的写法 RFC-0044 的说法 建议
§8 末段(rebuild_provider) “如果本文的 PR 先于 C6 合入,这一步就不需要了” C6 把 rebuild_provider 暂移到 ops,#270 的传输回放落地后删除(§3.1 表、§6 C6、§7) 本文 §10 把 schema 3 排在 C6 之后(第 2 步在第 3 步之前),这个条件不会出现。建议改成:C6 先把它移到 ops,第 3 步在 ops 里删掉。或者删掉这句
Q1 原提案(采集留在 http.rs:375) 只说 RFC-0044 改为最内层观察点 观察点包住用户或默认的叶子 Fetch,在鉴权之后、真正发出之前,和 D5 的 replay_fetch 是同一个接口(§4 第一行) 建议 Q1 补一条事实,供维护者判断:http.rs:375 交出的请求还没经过鉴权装饰层(SigV4Fetch、AnthropicAwsFetch、Azure 的 BearerTokenFetch、shared::package_fetch 的 UserAgentFetch);回放 Fetch 作为用户的 fetch 传入,在这些装饰层里面。所以采集看到的请求和回放看到的请求,在签名头、authorization、user-agent 上不一样,§5.2 的匹配如果用到 headers 就会受影响。观察点放在叶子上时,两边看到的是同一份请求
第 13 行 Related、§8 表头 RFC-0044(草稿) #279 已在 10-20 转为 ready,请 eric 审 去掉“草稿”

Q1、Q2、Q3 按待讨论保留原提案,这里不替维护者下结论。


Generated by Claude Code

…position

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

三处都已改,见最新提交。

位置 意见 处理
§8 末段、§8 表、§7 表、§10 第 3 步 条件“先于 C6 合入”不会出现 改为 C6 先把 rebuild_provider 移到 ops,§10 第 3 步在 ops 里删除
Q1 补充 http.rs:375 在鉴权装饰层之外、回放 Fetch 在装饰层之内的事实 已补:列出 SigV4Fetch、AnthropicAwsFetch、BearerTokenFetch、UserAgentFetch 四个装饰层;说明 Exact 已排除 user-agent 和敏感头,但 SigV4 的 x-amz-date、x-amz-content-sha256 仍会对不上;原提案保留待讨论,若退回原提案,需要把这些头加进排除集合
Related、§8 表头 去掉“草稿” 已去掉

Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

Q1 的补充事实按 li 的要求挪到了 issue:#270 (comment) 。RFC 正文的 Q1 恢复为开 PR 时的写法;上一条评论表格里 rebuild_provider 和“草稿”两处修正保留。


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

更正上一条:RFC 还开着,新发现直接进正文。Q1 的补充事实已放回 §11 Q1(72a054f8),#270 那条评论改为指向这里。


Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

方向正确,但当前文本还不可实现:

  • 阻塞 Fetch 层缺少模型调用身份:DiagnosticScope 只有 call_id,Exchange 却要 (seq, attempt, parent);重试和 MoA 子调用无法在叶子 Fetch 匹配。
  • 重要 modelRef 用 "<key>:<modelId>" 再按第一个 : 拆,和 registry 的可配置 separator 冲突,也丢了 namespace/method;建议直接用结构化引用。
  • 重要(安全) 响应分块原样落盘,但 RFC-0041 §5 要求在进入 writer 前做资格判定/脱敏;流式错误体里的凭证可能被持久化。
  • 重要 单次 exchange 上限 8 MiB 原始字节,JSONL 单行上限也是 8 MiB,base64 后必然超限;writer 合法的记录读不回来。
  • 重要 input 脱敏后仍标 live=true,live 回放会发送 [REDACTED]。
  • 重要 abandoned 映射为可重试的 FetchError::Io,回放可能意外重试,改变录制的 attempt 序列。
  • 重要 缺少 RFC-0041 要求的 terminal/phase 校验;迁移步骤没列全 schema-2 消费方(MockDecisionReplayModel、Web traces),且 replay.create 在 step 4 才引入的 ReplaySet 上被提前使用。

DiagnosticScope now carries (seq, attempt, parent), the per-attempt
exchange counter and observers, so §3, §4.1, §8 and Q3 cite it instead
of restating it. Provider tests that replay through ReplaySession move
to ops integration tests, since providers and devtools do not depend on
each other (RFC-0044 §8.1, RFC-0046 §5.9).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cunninghamcard-bit

Copy link
Copy Markdown
Contributor Author

c494403:按 RFC-0044 §4.2(adf6c618)与 RFC-0046 §5.9(fe5f2f07)更新。§3、§4.1、§8、Q3 的 (seq, attempt, parent) 与交换序号改为引用 DiagnosticScope,不再另定;§7、§10 第 4 步把依赖 ReplaySession 的 provider 回放测试移到 ops 集成测试,因为 providers 与 devtools 互不依赖(RFC-0044 §8 第 1 条)。

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

评审:RFC-0048 录制 schema 3 与传输回放 —— 批准(附 4 个待决问题的建议)

设计核验通过的关键点:

  1. 与 RFC-0041 的分工干净:格式约束/匹配语义/离线鉴权/错误码不重设计,本文只定接口、切片与顺序;schema 3 完整形状与 §4.1 逐字段吻合(Captured 联合、exchangeId 三元组、offsetUs 原点、replayability 写时算读时重算、metadata/payload 策略、8 MiB/4096 上限)。
  2. "回放就是一个 Fetch" 是正确解法:修复了 mock 只认 OpenAI chat 的根因(跳过 provider 解析器),与上游测试在 Fetch 层喂预制响应(msw)的做法同构;Exact/Sequential 的因果消费序((seq,attempt) 约束)正确处理重试与 MoA 并发;ReplayAmbiguous 同响应取最早、header 排除集合合理;429/5xx 作为普通响应走 provider 错误解析而非引擎错误——与 RFC-0041 §8.3 一致。
  3. live 回放改 modelRef(删除 provider() 截断猜测)正确关闭"重建目标靠猜"问题;ops 改动表完整(replay.offline 关 G-k、capabilities Future→Current 关 G-c、recorder.create 策略化)。
  4. Q1 的分析质量高:http.rs:375 与 Fetch 观察点两种位置的签名头不一致分析(SigV4 的 x-amz-date/x-amz-content-sha256)是决定性论据——观察点方案使录制与回放看到同一份请求。§4.1 按 RFC-0044 写是对的。
  5. 实施计划诚实:3→4 之间 master 无 mock 回放的缺口如实登记且给出合并选项;每个 PR 只做一个方向。

维护者对四个待决问题的建议(按 §11):

  • Q1:采用 RFC-0044 的 Fetch 观察点(即本文 §4.1 现状),删除原提案。理由如上第 4 点。
  • Q2:按 §10 排序(先建 aimux-devtools 再写 schema 3),不允许临时位置再搬。
  • Q4:接受 (b)——确定性一次性转换脚本不进仓库、在 PR 里列出供审核。转换是公开的、符合 RFC-0041 §2"不偷偷转存"的要求;脱敏头缺值在 §5.2 中已排除出比较,可回放。
  • Q5:保持 3、4 分开。缺口在两个未发布提交之间,不构成用户可见回归;换来两个可评审的中型 PR。

两个补充要求(随实施 PR 处理,不阻塞本 RFC):

  1. PR 3 需明确 schema-2 测试 fixture 的去向清单(aimux-core/tests/fixtures/master_*_recordings.jsonl 等 4 个 jsonl),不能只处理 Jev(Q4)。
  2. "outcome 不包 Captured"是对 RFC-0041 §4.1 字面的简化,落地时在 RFC-0041 加一行修订引用(与此前 §0.1.7/§7.1 修订同惯例)。

批准。

@eric8810

Copy link
Copy Markdown
Contributor

§11 四个待决问题的决定(接上条评审,作为维护者侧结论):

  • Q1:采用 RFC-0044 的 Fetch 观察点(即本文 §4.1 现状),原提案(http.rs:375)删除。理由:观察点方案下录制与回放看到同一份请求,避免 SigV4 签名头(x-amz-date/x-amz-content-sha256)对不上的根本问题。
  • Q2:按 §10 排序执行(PR 2 先建 aimux-devtools,PR 3 再写 schema 3),不允许临时位置再搬。
  • Q4:接受 (b)——确定性一次性转换,脚本不进仓库、在 PR 里列出供审核。
  • Q5:保持 PR 3、PR 4 分开;两 PR 间 master 无 mock 回放的缺口在未发布提交之间,可接受。

另请实施 PR 3 时附上 schema-2 测试 fixture 的去向清单(master_*_recordings.jsonl 等),并在落地时于 RFC-0041 补"outcome 不包 Captured"的修订行。

cunninghamcard-bit and others added 2 commits October 24, 2026 10:01
…review

§11 becomes the decided questions (Q1 observer, Q2 devtools first, Q4
one-off conversion, Q5 separate PRs). The 10-20 review items land in
the text: modelRef is the structured ops ModelRef; response chunks are
judged before the writer queue; one line limit shared by writer and
reader with a base64 budget; sensitive input is forbidden, so live is
off; abandoned replays as non-retryable ReplayBeyondRecording; reading
validates modelCalls, exchange ids, terminals and offsets; ReplaySet
moves to step 3; step 3 lists every schema-2 consumer and fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T

Copy link
Copy Markdown
Contributor Author

0c5a4682:合入 master(RFC-0044、0046、0047 已合并,Related 改链文件),并把评审意见和 §11 的决定写进正文。

10-20 评审的七条

eric 的意见(原话摘要) 处理 位置
阻塞:Fetch 层缺少模型调用身份 已在 c4944034 引用 RFC-0044 §4.2 的 DiagnosticScope;本次补明每个交换都带 modelCall(scope 总有 model_call,操作本身是 seq 0) §3
modelRef 用 "<key>:<modelId>" 按第一个 : 拆,与可配置 separator 冲突、丢 namespace/method 改为结构化引用,形状就是 ops 已有的 ModelRef { namespace, key, method, modelId };live 按 namespace 校验后精确取模型(同 resolve_model) §3、§4.3、§7
安全:响应分块原样落盘,违反 RFC-0041 §5 的进 writer 前判定 分块在 Recorder 里缓存到终态,进队列前判定:JSON 整段、SSE 逐个 data: 事件解析检查;含敏感字段、媒体类型未登记或无法判定的整段 forbidden §3、§9
单次交换 8 MiB 原始字节与 JSONL 单行 8 MiB 冲突,base64 后读不回 writer 与 reader 共用 RECORDING_LINE_LIMIT;采集按 base64 后大小记账(每条录制 6 MiB body 预算);序列化后仍超限就把 input 和 body 改记 missing: size-limit 再写,写出的每一行都能读回 §3、§5.1、§9
input 脱敏后仍 live=true,live 会发 [REDACTED] 会被 redact_json 改动的 input 整段记 forbidden: sensitive-field,不存脱敏副本;live 要求 input 为 captured §3
abandoned 映射为可重试的 FetchError::Io 吐完录下的分块后继续读,返回不可重试的 FetchError::Other,引擎错误 ReplayBeyondRecording;录下的 connect/timeout/io 仍可重试,因为录制里紧跟着那次重试 §5.1、§5.3
缺 terminal/phase 校验;迁移没列全 schema-2 消费方;replay.create 提前用了第 4 步的 ReplaySet 读取时校验 modelCalls 唯一无环、exchangeId 与序号、终态与偏移单调;phase 补上 prepare;ReplaySet 移到第 3 步;第 3 步列入 MockDecisionReplayModel、Web 录制导入导出 §3、§10

10-21 的决定与两个补充要求

eric 的意见 处理 位置
Q1 采用 Fetch 观察点,删除原提案 §11 改为“已定问题”,原提案删除,保留决定理由 §8、§11
Q2 先建 devtools,不先落临时位置 记为已定 §11
Q4 接受 (b),脚本不进仓库、PR 中列出 记为已定;转换放在第 3 步 §10、§11
Q5 3、4 分开 记为已定 §10、§11
PR 3 附 schema-2 fixture 去向清单 §10 加了四个 fixture 的去向表(两个 master_*_recordings.jsonl 随 schema 2 读取删除,Jev 转换,DTO 向量重写) §10
落地时在 RFC-0041 加 “outcome 不包 Captured” 修订行 写进第 3 步的内容 §9、§10

这次改动都在正文范围内,没有新的待定项,请再看一眼。


Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审后仍有阻塞项:1) terminal.error.message / outcome.error 仍是字符串,未走 RFC-0041 §5 的脱敏,provider body/prompt 可能落盘;2) SSE 判定把 OpenAI 的 data: [DONE] 当 unparsed,所有 OpenAI-compatible 流会不可回放;应把空 data、[DONE]、注释/event/id 当控制行;3) offline provider 默认取 modelRef.key,但 registry alias 不一定等于 model.provider(),应取 target.model.provider 或要求显式 provider;4) schema-2 迁移仍漏 jev_test.rs 的 jev_systemone_live.jsonl;5) 与 alignment doc 的字符串 modelRef/ per-call model 字段冲突未登记;6) MoA/router 子模型没有 offline 构造路径;7) prepare/auxiliary exchange 可能阻塞 model exchange 匹配;8) 录下的 connect/timeout/io 被假定会重试,最后一次 attempt 不成立。请修后再复审。

Error text follows the payload rules: outcome keeps name and code,
errorDetail and decisionResult are Captured and redacted, terminal
messages are policy-gated. SSE control lines ([DONE], comments, event,
id, retry, empty data) are not parsed. The offline package comes from an
exact table of each package's provider ids, never the registry alias.
Each model call records its model; replay.session and
replay.offlineModel let hosts compose router and MoA offline. Only model
exchanges are ordered. Retries follow the recorded options, including
the last attempt. The Jev fixture's three readers and the alignment doc
differences are listed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T

Copy link
Copy Markdown
Contributor Author

10-25 复审的八条在 f74d6a45 写进正文。

eric 的意见 处理 位置
1. terminal.error.message / outcome.error 是字符串,未走 RFC-0041 §5,body/prompt 可能落盘 outcome.error 只留 name、code;message 与 AI SDK JSON 移到 errorDetail: Captured,metadata 下禁止,payload 下 JSON 过 redact_json,会被改动就和 message 一起整段 forbidden,url 去 userinfo 和敏感 query;decisionResult 同样处理;terminal.error.message 改为 Captured<string>,metadata 下禁止,回放只靠 kind §3、§9
2. SSE 把 data: [DONE] 当 unparsed,OpenAI 兼容流都不可回放 注释行、event:、id:、retry:、空 data:、data: [DONE] 是控制行,不解析;其余事件的 data 拼起来按 JSON 检查 §3
3. 离线 provider 缺省取 modelRef.key,alias 不等于 model.provider() 不再用 key。新增 offline_provider_name(model_provider):各包在离线配方旁登记自己模型报告的完整 provider id,精确匹配,不按 . 截断;查不到就要求显式 provider §4.2、§6
4. 迁移漏了 jev_test.rs 对 Jev 录制的读取 去向表列出三处读者(decision_recording_test.rs:31/:253、jev_test.rs:112 经 common/replay.rs 的 wiremock、master_recording_test.rs:186);读 Recording 的 provider 测试在 C6 随录制类型移到 aimux-ops/tests/,第 3 步改读 schema 3,第 4 步换 ReplaySession §10
5. 与对齐文档的字符串 modelRef、逐调用 model 字段冲突未登记 §9 登记两处差异(结构化 modelRef 的理由;modelCalls 保留 model,不记 started_at 与逐调用 outcome,并说明从哪里得到);第 3 步同步改对齐文档 §4.3、§4.4、I5 行 §3、§9、§10
6. MoA/router 子模型没有离线构造路径 modelCalls 每条记 model { provider, modelId };新增 replay.session(会话句柄)与 replay.offlineModel { session, seq?, provider? }(按某个模型调用组装离线模型),宿主用已有的 composite.router / composite.moa 组装后交给 replay.offline { session, model? };子模型共用同一个会话的 fetch。有子调用却没给 model → ReplayNotPossible{composite-needs-model},因为组合策略不在录制里(RFC-0041 §9.1、对齐文档 §4.4) §3、§4.2、§5.1、§7
7. prepare/auxiliary 交换可能挡住 model 交换匹配 顺序约束只在 model 交换之间;非 model 交换不挡后面的交换,经过回放 Fetch 时在同一 (seq, attempt) 内按 Exact 匹配、不排序、不计入 verify_consumed;没采集也不影响 mock 可回放性 §3、§5.2
8. 录下的 connect/timeout/io 假定会重试,最后一次 attempt 不成立 改为:按 kind 还原、可重试性不变;是否重试由用录下的 maxRetries 跑的重试循环决定,所以与录制同路:要么有下一个 attempt,要么循环耗尽,返回同样的 RetryError;要了录制里没有的 attempt 就是 ReplayMiss(不可重试) §5.3

Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新 head f74d6a4 仍有三类阻塞:1) reader 要求 complete=true 时 status ∈ success/error/cancelled,但 writer 会合法写 incomplete+complete=true,ReplaySet::load 会拒绝合法录制;2) Exact 仍忽略 name-only header、比较敏感 query、对 multipart 做字节比较,和 RFC-0041 §7 不符;3) terminal.error.message 仍只做 URL 清理,未走 RFC-0041 §5 的 body/secret 策略。另有 deny_unknown_fields 与“后续加可选字段”冲突、Captured 缺少 not-applicable、ops-only error code/modelRef DTO 泄漏到 devtools、G-c 过度声明。请修后再复审。

cunninghamcard-bit and others added 2 commits October 25, 2026 08:35
- complete describes record completeness: any outcome status may pair
  with complete=true; reader no longer rejects legal incomplete records
- Exact follows RFC-0041 §7: per-package ExchangePolicy header sets,
  name-only non-auth headers make Exact unavailable, auth query dropped
  on both sides, multipart parsed, lossless JSON numbers
- error text follows RFC-0041 §5: errorDetail captured only when every
  field maps to captured sources; transport error stores kind only
- schema 3 field set closes at first release (deny_unknown_fields);
  WebSocket support bumps the schema
- Captured gains not-applicable; devtools owns RecordedModelRef and
  drops the ops error code; G-c stays open for credential.create

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T

Copy link
Copy Markdown
Contributor Author

回复 @eric8810 10-25 08:29Z 的评审,改动在 03235f9,随后合了 master,head 46ec22b。左列是 eric 的原话。

eric 的意见 处理 位置
reader 要求 complete=true 时 status ∈ success/error/cancelled,但 writer 会合法写 incomplete+complete=true,ReplaySet::load 会拒绝合法录制 删掉这条限制。complete 只说明记录是否完整(RFC-0041 §4.2),四个 status 都能和 complete: true 一起出现。现在只要求 transportClosed: true,而且每个交换都有终态。举例:流在 Finish 前 EOF 时写 incomplete(recording.rs:1863),这是一条完整录制,回放时得到同样的 EOF §3 读取
Exact 仍忽略 name-only header、比较敏感 query、对 multipart 做字节比较,和 RFC-0041 §7 不符 按 §7 重写。各包登记 ExchangePolicy,分 authentication、semantic、proven-nonsemantic 三类(类型放在 provider-utils,ops 把查找函数传进 Recorder 和 ReplaySession::new)。authentication 头在比较两边都排除;只记了名字的其他头会让这个交换的 Exact 不可用,记为 exact: 原因,返回 ReplayNotPossible,Sequential 仍可用。query 先在两边去掉认证参数,其余参数保留重复和顺序。multipart 按 MIME 解析,不看 boundary。JSON 数字按原文比较,不经 f64。normalizer 版本写在 ReplaySummary §3 headers、§5.1、§5.2、§6
terminal.error.message 仍只做 URL 清理,未走 RFC-0041 §5 的 body/secret 策略 传输错误不再存文字,只存 kind。原因是叶子 Fetch 的错误文字是任意文本,没法证明安全;回放时文字固定为 replayed <kind> error。errorDetail 只有在每个字段都能对应到本录制里已 captured 的来源时才保存,否则记 forbidden: unverified-text §3 错误文本、§5.3、§9
deny_unknown_fields 与“后续加可选字段”冲突 保留严格解析。schema 3 的字段集合在首个写出它的发布之后就封闭,以后加字段(可选字段也算)要升 schema 版本;WebSocket 那条改为升版本 §3 读取、§9
Captured 缺少 not-applicable 加上 not-applicable,并写清它能出现在哪里:无 body 的请求、无响应交换的 chunks、非错误结果的 errorDetail、非 decision 或非成功结果的 decisionResult。读取时校验它和事实一致;它与 RFC-0041 §4.1 的差异已登记 §3、§9
ops-only error code/modelRef DTO 泄漏到 devtools outcome.error 只保留 name,ops 错误码由 ops 按 name 算。devtools 定义自己的 RecordedModelRef,JSON 形状不变,ops 负责转换和校验 method。ReplayError 在 devtools 只带稳定名字,数字编码放在 ops §3、§4.3、§5.1、§7
G-c 过度声明 不再关闭 G-c,只把 replay.capabilities 改为已实现,credential.create 仍是 Future。capabilities 只报实际能力:matchers、pacing、有离线配方的包,WebSocket、下载、轮询都报 false §7、§10 第 5 步

另外核对了 Q4:Jev 的请求头只有 Authorization,属于 authentication,比较时排除,所以转换后仍能用 Exact 回放。


Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

仍有阻塞:reader 仍无条件要求 exchange terminal/timing,但 writer 合法写 complete=false 的 unfinished exchange;Jev 转换缺 transport-added content-type,Exact 会 miss;metadata 与 not-applicable 优先级冲突;replayability.mock 未覆盖 replay.offline 需要 captured input。请修后再复审。

- terminal and terminalOffsetUs are required only when complete=true;
  complete=false records may carry unfinished exchanges
- Captured state precedence: not-applicable before capture policy
- mock means transport replay (RFC-0041 §4.3); replay.offline needs a
  captured input or an explicit one, reported as operation: reasons
- Jev conversion restores the leaf headers build_fetch_request adds
  (content-type, user-agent); step 4 verifies it with an Exact replay

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T

Copy link
Copy Markdown
Contributor Author

@eric8810 你 11:06Z 那次评审 的四条已经改好,在 bc68ea1。下表左列是你的原话。

eric 的意见 处理 位置
reader 仍无条件要求 exchange terminal/timing,但 writer 合法写 complete=false 的 unfinished exchange 现在只在 complete: true 时要求 terminal 和 terminalOffsetUs。complete: false 的录制允许带没终结的交换,对应 writer 的两种兜底:recording.rs:1267、:1704。这类交换在 replayability 里记一条 unfinished:<exchangeId>。时间顺序只核对实际存在的值。 §3 读取
Jev 转换缺 transport-added content-type,Exact 会 miss 转换时把请求头还原成叶子 Fetch 实际看到的样子,也就是补上 build_fetch_request(http.rs:551)会加的两个头:user-agent(:585,proven-nonsemantic)和 content-type: application/json(:598,semantic)。第 4 步的 Jev 测试用 Exact 消费转换后的录制,再跑 verify_consumed(),靠这条测试验证转换结果。 §11 Q4 细则(决定不变)
metadata 与 not-applicable 优先级冲突 定了固定顺序:① 值本来不存在记 not-applicable;② 策略是 metadata 记 forbidden: capture-policy;③ 资格判定不通过记 forbidden;④ 超出预算或没观察到记 missing;⑤ 其余记 captured。值存不存在属于结构信息,metadata 下本来就会记录,所以 reader 的一致性校验在两种策略下都能做。 §3 Captured
replayability.mock 未覆盖 replay.offline 需要 captured input 按 RFC-0041 §4.3,mock 只指传输回放,不需要 input。重跑整个操作还要求 input 是 captured,否则记 operation:input:<reason>。replay.offline 新增可选参数 input:调用方没给、录下的 input 又不是 captured 时,返回 ReplayNotPossible。replay.inspect 分开报告这两种范围。 §3 replayability、§4.2

Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上一轮四个阻塞点都已修:complete=false 允许 unfinished、Jev Q4 恢复 content-type/user-agent、metadata 优先级排序、replay.offline 的 input。剩余均为实现前置/措辞 nit。Approve。

@eric8810
eric8810 merged commit 5157fdc into master Oct 25, 2026
14 checks passed
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.

2 participants