Skip to content

feat(antd): send retryable as gRPC metadata (x-antd-retryable) - #373

Merged
Nic-dorman merged 1 commit into
mainfrom
nic/v2-1630-antd-grpc-retryable-metadata
Oct 19, 2026
Merged

Nic-dorman merged 1 commit into
mainfrom
nic/v2-1630-antd-grpc-retryable-metadata

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Daemon half of V2-1630. Over gRPC, whether a failed paid write kept its payment now travels as structured metadata, not only in the message text.

Why

  • REST: the error body carries a structured retryable.
  • gRPC: only the error code was structured (x-antd-error-code). The retryable bit lived in the message tail: "(paid attempt retained: call finalize again with the same upload_id ...)".
  • Every gRPC SDK (C++, C#, Dart, Elixir, Go, Java, Kotlin, Python, Ruby, Rust, Swift) and antd-mcp end-anchors a parser on that suffix. A reworded message would silently remove the safe retry: the caller then re-prepares and pays twice, or gives up on a payment that could still be stored.
  • Hermes flagged this on feat(antd-java): pointer API (REST + gRPC) #331 (Java, "residual compatibility risk") and feat(antd-mcp): pointer tools #342 (MCP).

Change (antd/src/error.rs only)

  • x-antd-retryable: true|false on the gRPC status whenever retryable is known:
    • a CLOSE_GROUP_SHORTFALL from a pointer finalize that kept the paid state (Some(true)), next to its existing x-antd-error-code;
    • every PARTIAL_UPLOAD (always known: true = finalize again with the same upload_id, false = re-prepare).
  • Left off when unknown, as REST leaves retryable out.
  • AntdError::retryable() exposes the value for both transports.
  • Purely additive. Messages are unchanged, so existing clients keep working, and SDKs ignore unknown metadata keys.
  • Deliberately not added: an x-antd-error-code tag on PARTIAL_UPLOAD. It would be useful, but it changes what SDKs see on ABORTED, which CONFLICT shares, so it belongs in the SDK round where each mapper is checked.

Next (SDK half of V2-1630)

  • Each gRPC SDK prefers x-antd-retryable and keeps the suffix parser only as a fallback for daemons ≤ 0.15, with a regression test on both paths.
  • The fan-out docs get the key next to x-antd-error-code.
  • This waits for DO NOT MERGE: CI preview of the pointer + manifest fan-outs landed together #367, since the pointer error mappers live in the fan-out.
  • Landing this daemon half before antd 0.16.0 means no released pointer client ever depends on the message text.

Verified

  • cargo fmt --check clean.
  • cargo clippy --all-targets --all-features --locked -- -D warnings clean.
  • cargo test --locked: 149 passed. New test grpc_status_carries_retryable_as_metadata:
    • PARTIAL_UPLOAD with true and false;
    • a shortfall with Some(true) and with unknown retention;
    • an unrelated error carries no key;
    • the shortfall keeps its code tag.

Part of V2-1630.

🤖 Generated with Claude Code

REST tells a caller whether a failed paid write kept its payment with
the error body's structured `retryable`. gRPC carried only the error
code as metadata; the retryable bit was in the message text, so every
gRPC SDK end-anchors a parser on "(paid attempt retained: call finalize
again with the same upload_id ...)". Rewording that message would
silently drop the safe retry: the caller re-prepares and pays twice, or
gives up on a payment that could still be stored.

Attach `x-antd-retryable: true|false` to the gRPC status whenever
`retryable` is known: a pointer-finalize CLOSE_GROUP_SHORTFALL that kept
the paid state, and every PARTIAL_UPLOAD. It's left off when unknown,
as REST leaves the field out. Additive: clients that don't read it keep
parsing the message, which is unchanged.

Daemon half of V2-1630. Landing it before antd 0.16.0 means no released
pointer client ever depends on the message. The SDKs then prefer the
key and keep the suffix parser as a fallback for older daemons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

APPROVE — no blocking findings

Reviewed WithAutonomi/ant-sdk #373 at 9eb7634664647eb469ccb257f664eafd00f35187, targeting main at ea389c79c2ac40601c574e93031a66a9791db1d2. Scope: one changed file, antd/src/error.rs (+72/-4).

Contract and compatibility

  • retryable() exposes known true/false for PartialUpload, preserves the Option<bool> on CloseGroupShortfall, and returns None elsewhere.
  • with_retryable emits literal true/false only for known values. Unknown or inapplicable retention omits the key; omission is not false.
  • The status conversion preserves codes, messages and existing x-antd-error-code metadata. PARTIAL_UPLOAD deliberately gains no error-code tag. REST JSON is unchanged.
  • This reports whether the same finalize can reuse the retained upload_id and payment. It does not add a general automatic-retry policy. The existing finalize paths reinsert the paid session/resume handle before returning the retryable error. Those paths are unchanged.

Executed verification

Local macOS arm64, Rust/Cargo 1.96.0, dedicated exact-head worktree and revision-keyed target directory:

  • git diff --check ea389c79c2ac40601c574e93031a66a9791db1d2...HEAD: passed.
  • cargo fmt --all -- --check: passed.
  • cargo test --locked in antd: 149 passed, 0 failed.
  • cargo clippy --all-targets --all-features --locked -- -D warnings: passed.
  • 2 additional review-only tests passed, each with 19 variant/retention cases. One compares base/head Display strings, gRPC codes/messages/old metadata and REST status/body bytes. The other confirms metadata and UTF-8 messages survive a real tonic HTTP/2 loopback, including both boolean values, unknown retention and unrelated errors. These are separate from the 149 repository tests.

CI caveat — inherited dependency issue

Exact-head Check (antd), Check (antd-rust), Check (antd-js) and Third-party notices (antd) passed. Security audit remains red on zip 2.4.2: RUSTSEC-2026-0367, -0368 and -0369. Base and head antd/Cargo.lock are the same Git object (1f1e0acb2d947802bae11ae876b550f833276381), so the vulnerable dependency is inherited, not introduced here. This code approval does not waive the audit or branch protection.

Panel and limits

6/6 reviewed; 3/3 named models, both roles complete. GLM 5.3/OpenRouter, local Qwen 3.8 and local DS4 Flash each completed independent correctness and adversarial passes. All six recommend APPROVE; no verdict dissent or missing seat.

The loopback is a HealthService fixture using the actual error conversion, not a paid finalize end-to-end test. No live network/payment test or old-SDK binary test was run. SDK preference for this key plus legacy suffix fallback is the explicitly separate V2-1630 follow-up. Supplemental probes changed no tracked source. No merge performed.

@Nic-dorman
Nic-dorman merged commit 987f359 into main Oct 19, 2026
4 of 5 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