From 81a39ca0b9d7de2776d3f0b2df09bd94e2832d1d Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:48:17 +0900 Subject: [PATCH 1/3] fix(devin): parse generated approximate retry delays --- src/lib/retry-delay.ts | 4 +- structure/adapters/registry.md | 6 ++- .../devin-stated-reset-retry.test.ts | 38 +++++++++++++++++++ tests/server/retry-after-429.test.ts | 8 ++++ tests/server/retry-delay-hardening.test.ts | 3 ++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/lib/retry-delay.ts b/src/lib/retry-delay.ts index 31251770e2a..03db8ff6127 100644 --- a/src/lib/retry-delay.ts +++ b/src/lib/retry-delay.ts @@ -19,6 +19,7 @@ const MAX_COMPONENTS = 16; function durationSeconds(tail: string, allowBareSeconds: boolean): number | undefined { let rest = tail.trimStart(); + if (allowBareSeconds && rest.startsWith("~")) rest = rest.slice(1).trimStart(); let seconds = 0; let components = 0; while (true) { @@ -50,7 +51,8 @@ function durationSeconds(tail: string, allowBareSeconds: boolean): number | unde /** * Supports reset(s) in, try again in and Retry-After/retry after hints; accepts - * compound durations and rounds UP once after summing all components. + * compound durations, the generated Retry-After approximation marker, and + * rounds UP once after summing all components. * A bare number is permitted only for header-style Retry-After hints, never * for "reset in 2026". When a message declares several usable lower bounds, * honour the longest one rather than re-entering a still-live quota window. diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index caaed1dd3eb..26018ab8550 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -97,7 +97,11 @@ Some adapters share another adapter's routed-tool semantics while retaining inde with no evidence the adapter hint is omitted and the encoder still serializes its own 128000 fallback for field #3. Connect trailer diagnostics expose only an allowlisted error code, hexadecimal trace id and typed `retryAfterSeconds`, optionally rendered as - generated `retry after ~Ns` wording; raw text stays internal because it can reflect credentials. Investigation and limits: + generated `retry after ~Ns` wording; the shared retry-delay parser accepts that generated + approximation marker and preserves the same lower-bound delay when the diagnostic returns as an + outer error message. Each bounded replay evaluates its own typed delay or compatible message, so + a later refusal may change between the raw reset sentence and the generated diagnostic without + losing the next wait. Raw text stays internal because it can reflect credentials. Investigation and limits: `devlog/_plan/260917_devin_input_ceiling/000_review.md`. The registry records those relationships with `contractParent`. A parent relationship does **not** mean the registry recursively constructs a parent adapter and injects it into the child. Azure and MiMo keep owning their existing internal composition. This avoids making production constructors depend on test/conformance needs and keeps this authority refactor behavior-neutral. diff --git a/tests/providers/devin-stated-reset-retry.test.ts b/tests/providers/devin-stated-reset-retry.test.ts index 53356089d6f..a94b7117862 100644 --- a/tests/providers/devin-stated-reset-retry.test.ts +++ b/tests/providers/devin-stated-reset-retry.test.ts @@ -127,6 +127,44 @@ describe("streamChatEventsWithResetRetry", () => { expect(out.map(e => e.kind)).toEqual(["text", "finish"]); }); + test("waits the generated approximate retry delay and replays", async () => { + const waits: number[] = []; + let calls = 0; + const stream = () => { + calls += 1; + return calls === 1 + ? exhausting("Cognition chat failed (resource_exhausted); retry after ~180s")() + : events({ kind: "finish", reason: "stop" } as CloudChatEvent); + }; + const out = await drain(streamChatEventsWithResetRetry(REQ, { + stream, + sleep: async (ms) => { waits.push(ms); }, + })); + expect(calls).toBe(2); + expect(waits).toEqual([180_000]); + expect(out.map(e => e.kind)).toEqual(["finish"]); + }); + + test("re-evaluates the delay when retry failures use different wording", async () => { + const waits: number[] = []; + let calls = 0; + const stream = () => { + calls += 1; + if (calls === 1) return exhausting("Your limit will reset in 35 seconds")(); + if (calls === 2) { + return exhausting("Cognition chat failed (resource_exhausted); retry after ~180s")(); + } + return events({ kind: "finish", reason: "stop" } as CloudChatEvent); + }; + const out = await drain(streamChatEventsWithResetRetry(REQ, { + stream, + sleep: async (ms) => { waits.push(ms); }, + })); + expect(calls).toBe(3); + expect(waits).toEqual([35_000, 180_000]); + expect(out.map(e => e.kind)).toEqual(["finish"]); + }); + test("does not replay once any event was yielded", async () => { const stream = () => (async function* (): AsyncGenerator { yield { kind: "text", text: "partial" } as CloudChatEvent; diff --git a/tests/server/retry-after-429.test.ts b/tests/server/retry-after-429.test.ts index 8930ae55322..ba39f2cdef0 100644 --- a/tests/server/retry-after-429.test.ts +++ b/tests/server/retry-after-429.test.ts @@ -101,6 +101,14 @@ describe("resolveClientRetryAfter (#507)", () => { })).toBe("35"); }); + test("keeps a generated approximate Cognition delay in client cooldown metadata", () => { + expect(resolveClientRetryAfter({ + status: 429, + message: "Cognition chat failed (resource_exhausted); retry after ~180s", + includeDefault: false, + })).toBe("180"); + }); + test("reads a stated reset in minutes and hours, not just seconds", () => { expect(resolveClientRetryAfter({ status: 429, diff --git a/tests/server/retry-delay-hardening.test.ts b/tests/server/retry-delay-hardening.test.ts index e0e43d51c50..0a881473c51 100644 --- a/tests/server/retry-delay-hardening.test.ts +++ b/tests/server/retry-delay-hardening.test.ts @@ -17,6 +17,8 @@ describe("stated reset duration boundaries", () => { ["retry after 1500 milliseconds", 2], ["reset in 1 minute 500 milliseconds", 61], ["retry after 7.2s", 8], + ["retry after ~180s", 180], + ["Retry-After: ~3 minutes", 180], ["Retry-After: 30", 30], ["Retry-After: 0.1", 1], ["Your limit RESETS IN 21 MINUTES", 1260], @@ -33,6 +35,7 @@ describe("stated reset duration boundaries", () => { "reset in 5 minutes 30", "reset in 5 months", "retry after 3 monkeys", + "reset in ~3 minutes", "try again in 1e3s", "Retry-After: 3:30", "Retry-After: 123abc", From 979e209fbcc6a44aad4d13df6f8a519c29d9aaef Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:09:03 +0900 Subject: [PATCH 2/3] docs(devin): document stated-reset retry and approximate delay marker --- docs-site/src/content/docs/reference/adapters.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 183dcf8636e..bcc1440a32d 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -495,6 +495,13 @@ configuration that names the old id is rewritten at startup. `CompletionConfiguration`, #2 is the output cap and #3 is the context window; swapping those two makes every turn fail with an opaque `invalid_argument`. A temperature of exactly 0 is refused, so it is clamped to the smallest accepted value. +- A pre-output 429 that states a recovery delay is retried in place: the adapter waits the full + stated delay and replays the request up to twice, with a cumulative wait capped at 30 minutes + (`OPENCODEX_DEVIN_STATED_RESET_WAIT_MS`, hard ceiling one hour). Retrying earlier than the + stated delay is deliberately not attempted — the hint is the provider's best estimate of its own + window, and each replay slot is finite. If the limit still refuses, the final 429 surfaces to the + client with the stated delay preserved as its cooldown hint; a `~` in that hint marks a delay + recovered from a secondhand message, so it is approximate rather than an exact header value. - Experimental unofficial bridge; not shown in the dashboard preset by default. See the [provider guide](/guides/providers/) for login instructions. From 5cff45fba41f1b426ca20bf7fcdb1f66c4f674de Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:05:08 +0900 Subject: [PATCH 3/3] docs(devin): qualify stated-reset retry conditions and approx marker --- .../src/content/docs/reference/adapters.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index bcc1440a32d..313eeb3ef33 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -495,13 +495,16 @@ configuration that names the old id is rewritten at startup. `CompletionConfiguration`, #2 is the output cap and #3 is the context window; swapping those two makes every turn fail with an opaque `invalid_argument`. A temperature of exactly 0 is refused, so it is clamped to the smallest accepted value. -- A pre-output 429 that states a recovery delay is retried in place: the adapter waits the full - stated delay and replays the request up to twice, with a cumulative wait capped at 30 minutes - (`OPENCODEX_DEVIN_STATED_RESET_WAIT_MS`, hard ceiling one hour). Retrying earlier than the - stated delay is deliberately not attempted — the hint is the provider's best estimate of its own - window, and each replay slot is finite. If the limit still refuses, the final 429 surfaces to the - client with the stated delay preserved as its cooldown hint; a `~` in that hint marks a delay - recovered from a secondhand message, so it is approximate rather than an exact header value. +- A pre-output 429 that states a recovery delay is retried in place only when the full stated + delay fits within the remaining cumulative wait allowance. The adapter waits that full delay + and replays the request up to twice; the default cumulative allowance is 30 minutes + (`OPENCODEX_DEVIN_STATED_RESET_WAIT_MS`, hard ceiling one hour). If the delay exceeds the + remaining allowance, the original 429 is surfaced without waiting or replaying. Retrying + earlier than the stated delay is deliberately not attempted — the hint is the provider's best + estimate of its own window, and each replay slot is finite. If the limit still refuses, the + final 429 surfaces to the client with the stated delay preserved as its cooldown hint. A `~` + in the surfaced message marks a delay recovered from a secondhand trailer sentence rather + than an exact header value; clients still receive the parsed number itself. - Experimental unofficial bridge; not shown in the dashboard preset by default. See the [provider guide](/guides/providers/) for login instructions.