Skip to content

[finding] the protocol docs teach details.retry_after on a 429 while the runtime emits details.retryAfterSeconds — a client written from the docs reads undefined #15855

Description

@claude

Found while classifying retryAfter occurrences for #15677 (stack card 2/6 of #14478). Out of that card's scope — it is about the details bag, not the envelope key that card renames — and pre-existing on origin/main, so it is filed rather than fixed.

Two pages teach two different spellings for the same details member

Both describe the 429 RATE_LIMIT_EXCEEDED error envelope, and both are hand-written protocol docs.

page what it teaches occurrences on origin/main
content/docs/protocol/kernel/http-protocol.mdx:1126 "details": { "retryAfterSeconds": 45, "resetAt": … } 1
content/docs/protocol/kernel/error-handling.mdx:418, 526, 650 "details": { "retry_after": 45 } 3
content/docs/protocol/kernel/realtime-protocol.mdx:1022 "retry_after": 5 1

error-handling.mdx:734 then teaches a client to read it as data.error.details.retry_after.

Which one is right — measured, not guessed

packages/runtime/src/endpoint-policy.ts emits:

details: { retryAfterSeconds: retryAfterSec, resetAt: new Date(decision.resetAt).toISOString() },

and two tests pin that spelling (endpoint-policy.test.ts, dispatcher-plugin.endpoint-fallback.integration.test.ts). So http-protocol.mdx matches the runtime and the other three pages do not: a client written from error-handling.mdx reads details.retry_after, gets undefined on every real 429, and falls back to its default backoff silently.

⚠️ Do not fix this by sweeping retry_after globally. The same token is also a legitimate RetryStrategy enum value (packages/spec/src/api/errors.zod.tsz.enum(['no_retry', 'retry_immediate', 'retry_backoff', 'retry_after'])), and content/docs/api/error-catalog.mdx:47, 305, 391, 396 use it correctly in that sense. Only the details members are wrong.

Why nothing catches it

details is z.unknown() on the envelope, so no schema constrains its members and no gate compares a documented details key against what any producer emits. The os:check type-check does not reach it either: these are JSON fences, not TypeScript.

Suggested fix

Move the three error-handling.mdx sites and the realtime-protocol.mdx site onto retryAfterSeconds, matching the runtime, and re-check the realtime-protocol.mdx one against its own producer first — realtime may have a different emitter, in which case it is a second finding rather than the same one.

Measured on origin/main at cc5b3dd0c (2026-09-05). Related: #14478, #15677.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions