From 2faf7bfd48715a9878a594c401861d2eb392fa56 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 21 Jun 2026 13:43:01 -0600 Subject: [PATCH] =?UTF-8?q?plan(SP22):=20Task=204=20=E2=80=94=20distinguis?= =?UTF-8?q?h=204xx=20(terminal)=20from=205xx=20(retry)=20+=20drop=20dead?= =?UTF-8?q?=20BACKOFF=5FS[2]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update _get_typed in the plan to: - raise immediately on 4xx (terminal) - retry on 5xx and RequestError (transient) - use BACKOFF_S = (1.0, 2.0) instead of (1.0, 2.0, 4.0) - use RuntimeError instead of assert for the context-manager guard Also add test_4xx_is_terminal_not_retried to the plan's test list. --- .../2026-06-21-cyclone-pipeline-agent.md | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md b/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md index ded566c..14cca1e 100644 --- a/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md +++ b/docs/superpowers/plans/2026-06-21-cyclone-pipeline-agent.md @@ -747,7 +747,7 @@ T = TypeVar("T", bound=BaseModel) DEFAULT_TIMEOUT = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=5.0) MAX_RETRIES = 3 -BACKOFF_S = (1.0, 2.0, 4.0) +BACKOFF_S = (1.0, 2.0) # only used between attempts 0→1 and 1→2 # --- Response models --------------------------------------------------------- @@ -820,24 +820,31 @@ class CycloneClient: async def _get_typed( self, path: str, model: type[T] ) -> T: - assert self._http is not None, "use `async with CycloneClient(...)`" + if self._http is None: + raise RuntimeError("use `async with CycloneClient(...)`") last_exc: Exception | None = None for attempt in range(MAX_RETRIES): try: resp = await self._http.get(path) resp.raise_for_status() return model.model_validate(resp.json()) - except (httpx.HTTPStatusError, httpx.RequestError) as e: + except httpx.HTTPStatusError as e: + # 4xx is terminal — surface immediately, do not retry + if e.response is not None and e.response.status_code < 500: + raise last_exc = e - if attempt < MAX_RETRIES - 1: - wait_s = BACKOFF_S[attempt] - log.warning( - "api_get_retry", - path=path, attempt=attempt + 1, wait_s=wait_s, - error=str(e), - ) - await asyncio.sleep(wait_s) - assert last_exc is not None + except httpx.RequestError as e: + # connect/read failures are transient — retry + last_exc = e + if attempt < MAX_RETRIES - 1: + wait_s = BACKOFF_S[attempt] + log.warning( + "api_get_retry", + path=path, attempt=attempt + 1, wait_s=wait_s, + error=str(last_exc), + ) + await asyncio.sleep(wait_s) + assert last_exc is not None # loop body always sets this on the way out raise last_exc # --- methods -----------------------------------------------------------