{"record":{"id":"ec2f5a910b5be1db","repo":"heygen-com/hyperframes","slug":"render-lastdetail-render-id-did-not-reach-a-ter","errorCode":null,"errorMessage":"Render ${lastDetail.render_id} did not reach a terminal state within ${Math.round(elapsedMs / 1000)}s","messagePattern":"Render (.+?) did not reach a terminal state within (.+?)s","errorType":"exception","errorClass":"PollTimeoutError","httpStatus":null,"severity":"error","filePath":"packages/cli/src/cloud/poll.ts","lineNumber":83,"sourceCode":"  // instead of waiting out the full interval. Tests inject a no-op\n  // sleep that ignores the signal — that's fine, they don't abort.\n  const sleep = options.sleep ?? defaultAbortableSleep(options.signal);\n\n  const started = now();\n\n  while (true) {\n    if (options.signal?.aborted) {\n      throw signalAbortError(options.signal);\n    }\n    const detail = await client.getRender({ render_id: renderId, signal: options.signal });\n    const elapsed = now() - started;\n    options.onTick?.(detail, elapsed);\n\n    if (isTerminal(detail.status)) {\n      return detail;\n    }\n    if (elapsed >= maxWaitMs) {\n      throw new PollTimeoutError(detail, elapsed);\n    }\n    await sleep(intervalMs);\n  }\n}\n\nfunction signalAbortError(signal: AbortSignal): Error {\n  const reason = signal.reason;\n  return reason instanceof Error ? reason : new Error(\"Poll aborted\");\n}\n\nfunction defaultAbortableSleep(signal?: AbortSignal): (ms: number) => Promise<void> {\n  // fallow-ignore-next-line complexity\n  return (ms: number) =>\n    new Promise<void>((resolve, reject) => {\n      const onAbort = (): void => {\n        clearTimeout(timer);\n        reject(signalAbortError(signal!));\n      };","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/cli/src/cloud/poll.ts#L65-L101","documentation":"PollTimeoutError is thrown by pollUntilTerminal when the render has not reached 'completed' or 'failed' within maxWaitMs (default 60 min, interval 10s). It is a dedicated Error subclass that carries lastDetail (the most recent GET /v3/hyperframes/renders/{id} response) so callers can inspect status, progress, and any partial error info. Underlying request errors (404/401/5xx) bubble immediately and are intentionally NOT retried inside the loop.","triggerScenarios":"A render that stays in 'queued'/'processing' longer than maxWaitMs; a Temporal workflow whose start_to_close is longer than the poll cap; the API stalling without flipping to a terminal status; maxWaitMs explicitly lowered by the caller for a fast-fail policy.","commonSituations":"A genuinely long render (high-resolution, many scenes) exceeding the default cap; a stuck server-side workflow; calling poll with a too-small maxWaitMs; network blips that repeatedly delay each getRender without erroring.","solutions":["Catch PollTimeoutError specifically, read err.lastDetail, and decide: resume polling with another pollUntilTerminal call, or surface failure.","If the render is legitimately long, raise maxWaitMs (e.g. 2h) and pass a generous intervalMs.","Check the render id via `hyperframes cloud status <id>` or the dashboard to see if the workflow is actually progressing.","If lastDetail.status is stale across many ticks, report a stuck workflow to the API owner rather than looping forever."],"exampleFix":"// before: single 60-min cap, hard fail on timeout\nconst detail = await pollUntilTerminal(client, id);\n\n// after: resume across multiple windows, keep lastDetail\nlet detail;\ntry {\n  detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });\n} catch (err) {\n  if (err instanceof PollTimeoutError) {\n    detail = err.lastDetail;          // inspect, then resume or report\n    detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });\n  } else throw err;\n}","handlingStrategy":"try-catch","validationCode":"// Pre-check: confirm the render id exists before polling\nconst detail = await client.getRender({ render_id: id });\nif (!detail) throw new Error(`render ${id} not found`);","typeGuard":"function isPollTimeoutError(err: unknown): err is PollTimeoutError {\n  return err instanceof Error && err.name === 'PollTimeoutError';\n}","tryCatchPattern":"try {\n  detail = await pollUntilTerminal(client, id, { maxWaitMs: 30 * 60_000 });\n} catch (err) {\n  if (err instanceof PollTimeoutError) {\n    detail = err.lastDetail;  // inspect, then resume or escalate\n  } else throw err;\n}","preventionTips":["Size maxWaitMs to the worst-case render duration, not the average.","Use err.lastDetail to decide resume vs. escalate rather than looping blindly.","Surface stuck workflows (status unchanged across ticks) to the API owner."],"tags":["cloud","polling","timeout","render"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}