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.ts — z.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
Found while classifying
retryAfteroccurrences for #15677 (stack card 2/6 of #14478). Out of that card's scope — it is about thedetailsbag, not the envelope key that card renames — and pre-existing onorigin/main, so it is filed rather than fixed.Two pages teach two different spellings for the same
detailsmemberBoth describe the
429 RATE_LIMIT_EXCEEDEDerror envelope, and both are hand-written protocol docs.origin/maincontent/docs/protocol/kernel/http-protocol.mdx:1126"details": { "retryAfterSeconds": 45, "resetAt": … }content/docs/protocol/kernel/error-handling.mdx:418, 526, 650"details": { "retry_after": 45 }content/docs/protocol/kernel/realtime-protocol.mdx:1022"retry_after": 5error-handling.mdx:734then teaches a client to read it asdata.error.details.retry_after.Which one is right — measured, not guessed
packages/runtime/src/endpoint-policy.tsemits:and two tests pin that spelling (
endpoint-policy.test.ts,dispatcher-plugin.endpoint-fallback.integration.test.ts). Sohttp-protocol.mdxmatches the runtime and the other three pages do not: a client written fromerror-handling.mdxreadsdetails.retry_after, getsundefinedon every real 429, and falls back to its default backoff silently.retry_afterglobally. The same token is also a legitimateRetryStrategyenum value (packages/spec/src/api/errors.zod.ts—z.enum(['no_retry', 'retry_immediate', 'retry_backoff', 'retry_after'])), andcontent/docs/api/error-catalog.mdx:47, 305, 391, 396use it correctly in that sense. Only thedetailsmembers are wrong.Why nothing catches it
detailsisz.unknown()on the envelope, so no schema constrains its members and no gate compares a documenteddetailskey against what any producer emits. Theos:checktype-check does not reach it either: these are JSON fences, not TypeScript.Suggested fix
Move the three
error-handling.mdxsites and therealtime-protocol.mdxsite ontoretryAfterSeconds, matching the runtime, and re-check therealtime-protocol.mdxone 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/mainatcc5b3dd0c(2026-09-05). Related: #14478, #15677.Generated by Claude Code