Skip to content

refactor!: drop retryAfterMs from the error envelope - #349

Merged
eric8810 merged 6 commits into
masterfrom
refactor/drop-retry-after-ms-from-the-error-envelope
Oct 25, 2026
Merged

eric8810 merged 6 commits into
masterfrom
refactor/drop-retry-after-ms-from-the-error-envelope

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li · project thread

Summary

Before: an ops error envelope carried retryAfterMs next to error.responseHeaders. To compute it once, ops threaded a clock through dispatch, frames and operations and cached the projected envelope.

After: the envelope is {kind, code, message, error} and depends only on the error. Hosts that need to wait read error.responseHeaders (retry-after-ms, then retry-after), as AI SDK users do; upstream APICallError has no retry-hint field either.

Merged with master b69137e (the error-cause series #289/#290/#293); bindings regenerated.

Overview: how a retry delay reaches each caller today (master b69137e)

Rust caller
AiMuxError::ApiCall(ApiCallError)                         aimux-core/src/error.rs
├ response_headers                                         same field set as upstream APICallError
└ retry_after_hint() → retry_after_ms_from_headers(.., now)   read at call time, not stored

core retry and provider poll
retry_with_exponential_backoff → retry_header_delay       aimux-core/src/retry.rs
shared poll                                               aimux-providers/src/shared/poll.rs
└ AiMuxError::retry_after_hint()                          upstream: getRetryDelayInMs, retry-with-exponential-backoff.ts:10

binding caller (C / Node / Python / Go / Java / Kotlin / Swift / Dart)
dispatch → OpsError::project_with_clock(ctx.now)          aimux-ops/src/dispatch.rs
└ project(now()) → Projected { envelope, source }         aimux-ops/src/envelope.rs
  └ envelope(now) → retryAfterMs = retry_hint::retry_after_ms(headers, now)
frame end / Operation::start_with_clock                    aimux-ops/src/frame.rs, ops/operation.rs

This PR

binding caller
dispatch → Err(OpsError)                                  aimux-ops/src/dispatch.rs
└ adapter / frame end / operation terminal → error.envelope()   aimux-ops/src/envelope.rs:87 {kind, code, message, error}
  host reads error.responseHeaders itself                  as upstream JS users do
core retry and provider poll → AiMuxError::retry_after_hint()   unchanged

Commits:

commit change
ff58c2d refactor! removes ErrorEnvelope.retry_after_ms, Ctx.now, OpsError::Projected / ProjectedError / project / project_with_clock / unprojected, Operation::start_with_clock, aimux-ops/src/retry_hint.rs; parser golden cases move into aimux-core/src/error.rs; bindings drop their forwarding code; RFC-0039 §6.1/§6.2, implementation notes, alignment doc, READMEs, CHANGELOG
b557c9b merge master's error-cause series; ops tests keep InvalidParams { .. } without unprojected(), binding READMEs keep the cause text without retryAfterMs; codegen rerun
862be4b test nested_retry_keeps_the_attempt_hint (aimux-core/src/error.rs): no hint at either retry level, the attempt keeps its own through nesting and serde
eea8e80 fix(codegen) Python folds an explicit null on an optional field, as Rust does (Ty::keeps_null, tools/aimux-codegen/src/model.rs); only JSON values and presence fields keep it. Found by the new expect vector: Python re-encoded {"output": null}
a5f967f test(dto) ModelLimits/002 (u64::MAX: D13, D15), 003 (2^53+1: D13, still checked in Java/Dart), 004 + 004.expect.json (explicit null: D16); index, sources, D13/D15/D16 registrations and vector counts
37ac2d1 docs legacy banner on docs/API.md and docs/api/{c,flutter,go,java,kotlin,node,python,swift}.md (they describe the whole 0.5 binding API; rewrite tracked in #277); ABI-MIGRATION.md names retry-after-ms before retry-after

Closes #334

Type of change

  • Breaking change
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes (Rust 1.99)
  • cargo test --workspace passes
  • Added or updated cassette tests / contract fixtures for behavior changes
  • Updated relevant docs
  • Commit messages follow Conventional Commits

Also run locally after the merge: cargo run -p aimux-codegen -- --check; scripts/check_provider_boundaries.sh; check_dto_vectors.py python node (240 vectors); Python test_dto_types.py. Go, Java, Dart and Swift vector runs are left to CI.

Notes for reviewers

@cunninghamcard-bit cunninghamcard-bit self-assigned this Oct 20, 2026
cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
Retry's lastError as every binding's native cause, the Node and Java
cause classes, an accurate security section with a size and redaction
question, the full list of breaking changes, Swift's WireIndirect, and
how RFC-0044, RFC-0048 and #349 meet this RFC. Q2 and Q3 are marked
adopted.

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

@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

核心/ops 和绑定侧都没有发现阻塞项:retryAfterMs、Ctx.now、Projected、retry_hint 删除完整;生成的 ErrorEnvelope、C 头文件和各语言绑定一致;core retry 和 provider poll 仍从 responseHeaders 读 hint。建议修:

  • 重要 当前语言 API 指南仍宣传已删除的字段:docs/api/c.md、node.md、python.md、go.md、java.md、kotlin.md、swift.md、flutter.md 还有 retryMs / retry_ms / aimux_error_retry_ms。这是 breaking PR,发布点会让用户照着旧文档写出不存在的调用;请更新,或在 docs/API.md 加显眼的 legacy banner 并开跟踪 issue。
  • 次要 删除 ErrorEnvelope/002.json / 002.expect.json 后,DTO 的 expect 分支和显式 null 折叠(D16)没有向量;ErrorEnvelope/001 删除后也没有 >2^53 整数向量,D13/D15 的 carve-out 变成未覆盖。建议换一个普通可选字段补 expect 向量,并在别的类型补一个 u64 边界向量,或删掉 carve-out。
  • 次要 aimux-core/src/error.rs 的新测试没有覆盖 Retry-in-Retry / 保留 attempt hint;补一个嵌套 aggregate。
  • 次要 aimux-ffi/ABI-MIGRATION.md 只写 retry-after,应写优先 retry-after-ms 再 retry-after。
  • 提示 base c0622ea4 不是 head 的祖先(merge-base 是 b91829af),当前 PR 显示冲突;合并前 rebase,避免把 #287/#283 的改动回退。

eric8810 pushed a commit that referenced this pull request Oct 21, 2026
* docs(rfc): RFC-0047 error system and error cause

Describes how aimux's error system differs from the AI SDK's, traces
today's error paths from the public entry functions to the lines that
drop the underlying error, and designs `cause` across the core, the
transport layer, the ops envelope, the generated DTOs and every binding.
The open questions in section 10 stay open. Line numbers refer to
master 4220847, and the text includes what implementing #289, #290
and #293 found.

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

* docs(rfc): answer the RFC-0047 review

Retry's lastError as every binding's native cause, the Node and Java
cause classes, an accurate security section with a size and redaction
question, the full list of breaking changes, Swift's WireIndirect, and
how RFC-0044, RFC-0048 and #349 meet this RFC. Q2 and Q3 are marked
adopted.

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

---------

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

@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.

评审:drop retryAfterMs from the error envelope —— 批准(rebase 后合并)

规范出处核清:RFC-0047 §11.1 只是预案此变更,真正的决定在 issue #334 与 RFC-0039 §6——本 PR 正确修订了 RFC-0039(§6.1 示例与字段表、§6.2 重写为"信封没有派生 retry hint"、#166 C3 行同步)。

语义不丢:宿主直接读 error.responseHeaders(与上游 APICallError 一致);core 保留唯一解析器 retry_after_ms_from_headers(retry.rs/poll.rs 原样使用);§6.2 派生规则的 golden 向量(ms 优先/秒/HTTP-date/过去日期/大小写/溢出边界/优先级)整体迁入 error.rs 并新增"聚合错误不继承尝试 hint"的固化断言。删除的 1054 行主要是时钟穿线、投影缓存与生成物;顺带消除了 stream 终态用 UNIX_EPOCH 假时钟算 HTTP-date hint 的死语义。D13/D15 的大整数 carve-out 随 ErrorEnvelope/002 删除而休眠,已如实登记。

三个跟进项(非阻塞):

  1. 与 master 的 11 处冲突全部来自 [1/3] 的 digest/golden——rebase 后重跑 codegen 即可;与 #290/#293 谁后合谁解 envelope 冲突(RFC §11.1 已预案);
  2. D16 的 expect 路径现无向量覆盖,建议补一个其他类型的 expect 向量;
  3. docs/api/*.md 仍描述 0.5 的 retryMs(预先存在的过时),建议本 PR 或紧跟的 docs PR 收掉。

批准(rebase + codegen 重跑后合并)。

The ops error envelope is now {kind, code, message, error}. The derived
retryAfterMs duplicated error.responseHeaders and forced ops to carry a
clock (Ctx.now), a cached projection (OpsError::Projected),
Operation::start_with_clock and a re-export of the core header parser.
Upstream APICallError has no such field; hosts read responseHeaders, and
core still computes delays itself when it retries or polls.

Bindings regenerate their DTOs and drop the forwarding code; the
ErrorEnvelope vectors, RFC-0039 §6 and the implementation notes follow.

Closes #334

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cunninghamcard-bit
cunninghamcard-bit force-pushed the refactor/drop-retry-after-ms-from-the-error-envelope branch from 3b4c809 to ff58c2d Compare October 22, 2026 06:32

@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.

复审(rebased head ff58c2d)

之前的 rebase/冲突已解决,但先前指出的发布阻塞项仍在,暂不能合并:

  • 重要 当前 docs/api/c.md、flutter.md、kotlin.md、node.md、python.md、swift.md 仍宣传已删除的 retryMs / retry_ms / aimux_error_retry_ms。这是 breaking PR 的发布点,用户会照着旧 API 写不存在的调用;请更新,或明确加 legacy banner + 跟踪 issue。
  • 次要 contract-tests/dto 只剩 ErrorEnvelope/001.json;expect 分支和显式 null 折叠(D16)没有向量,>2^53 的 D13/D15 carve-out 也不再有覆盖。建议用普通可选字段补 expect 向量,并在别的类型补 u64 边界向量,或删除 carve-out。
  • 次要 aimux-ffi/ABI-MIGRATION.md:18 只写 retry-after,应写优先 retry-after-ms 再 retry-after。
  • 次要 新测试仍未覆盖 Retry-in-Retry / attempt hint 保留;建议补嵌套 aggregate。

之前的 APPROVED 是针对旧 head 的,当前 head 修完这些后我再复审。请求修改。

cunninghamcard-bit and others added 5 commits October 24, 2026 10:07
Brings in the error-cause series (#289, #290, #293). Conflicts: ops
tests take master's `InvalidParams { .. }` without `unprojected()`;
envelope.rs keeps `BindingError::result_serialization` without a clock;
binding READMEs and Node error.ts keep the cause text without
retryAfterMs. Bindings regenerated with aimux-codegen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T
Neither retry level has a hint of its own; the innermost attempt keeps
retry-after-ms through nesting and a serde round trip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T
Python kept null on every optional field whose schema is nullable, so
it re-encoded {"output": null} where Rust and the other codecs omit the
key. Only JSON values and presence fields keep null now
(Ty::keeps_null replaces accepts_null).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T
ModelLimits/002 (u64::MAX) and 003 (2^53+1) exercise the D13 and D15
carve-outs; 004 with 004.expect.json is an explicit null that folds to
an omitted key (D16). They replace the two ErrorEnvelope vectors removed
with retryAfterMs. The registrations and vector counts follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T
docs/API.md and docs/api/{c,flutter,go,java,kotlin,node,python,swift}.md
still describe the 0.5 binding API, including the removed retry hint;
each now says so at the top and points to the binding README and #277.
ABI-MIGRATION reads retry-after-ms before retry-after.

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

复审意见(ff58c2da)在 37ac2d14 处理完,同时合入了 master(b69137e,含 #293 的 envelope 冲突)并重新生成了绑定。

eric 的意见 处理 提交
重要:docs/api/*.md 仍宣传已删除的 retryMs / retry_ms / aimux_error_retry_ms;更新,或加 legacy banner + 跟踪 issue 加 banner:docs/API.md 和 c、flutter、go、java、kotlin、node、python、swift 八份指南顶部写明是 0.5 API、V4 信封没有 retry hint、等待时间读 error.responseHeaders,指向各绑定 README(C 另指 ABI-MIGRATION.md)。这些指南整份都是 0.5 的字段 getter,不只 retry 一处,所以没有逐行改;重写登记在 #277 的 API 文档一项 37ac2d1
次要:expect 分支和显式 null 折叠(D16)没有向量;>2^53 的 D13/D15 carve-out 无覆盖 补 ModelLimits 三个向量:002 为 u64::MAX(Node 按 double 比、Java/Dart 跳过),003 为 2^53+1(Node 按 double 比、Java/Dart 照常检查),004 配 004.expect.json 为可选字段上的显式 null。D13/D15/D16 的登记和向量数随之更新 a5f967f
(新向量发现的问题) 004 在 Python 上失败:生成的模型对 schema 可空的可选字段保留显式 null,与 Rust 和登记的 D16 不符。生成器改为只有 JSON 值和 presence 字段保留 null(Ty::keeps_null),其余可选字段折叠为省略 eea8e80
次要:ABI-MIGRATION.md:18 只写 retry-after 改为先 retry-after-ms 再 retry-after 37ac2d1
次要:缺 Retry-in-Retry / attempt hint 保留 新增 nested_retry_keeps_the_attempt_hint:两层 retry 都没有 hint,最内层 attempt 经嵌套和 serde 往返后仍是 250 862be4b

本地 1.99 的 fmt、clippy、workspace 测试、codegen --check、边界脚本、Python/Node 向量都过了,Go、Java、Dart、Swift 看 CI。


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.

复审通过:docs/api 的 legacy banner + #277、ModelLimits 002/003/004 向量、ABI 的 retry-after-ms 优先级、嵌套 Retry 测试都已补;22 项检查通过。Approve。

@eric8810
eric8810 merged commit 06cf695 into master Oct 25, 2026
22 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.

错误信封去掉 retryAfterMs,ops 不再处理时间

2 participants