{"record":{"id":"8334ecd8508fa62e","repo":"nexu-io/open-design","slug":"leonardo-ai-generation-timed-out-after-2-minutes","errorCode":null,"errorMessage":"leonardo.ai generation timed out after 2 minutes","messagePattern":"leonardo\\.ai generation timed out after 2 minutes","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2283,"sourceCode":"      throw new Error(`leonardo.ai poll ${pollResp.status}`);\n    }\n    \n    const pollData = (await pollResp.json()) as Record<string, any>;\n    const generation = pollData?.generations_by_pk;\n    \n    if (generation?.status === 'COMPLETE') {\n      const images = generation?.generated_images;\n      if (Array.isArray(images) && images.length > 0) {\n        imageUrl = images[0]?.url;\n        break;\n      }\n    } else if (generation?.status === 'FAILED') {\n      throw new Error('leonardo.ai generation failed');\n    }\n  }\n  \n  if (!imageUrl) {\n    throw new Error('leonardo.ai generation timed out after 2 minutes');\n  }\n  \n  // Fetch the generated image\n  const imgResp = await fetch(imageUrl, withMediaRequestInit(ctx));\n  if (!imgResp.ok) {\n    throw new Error(`leonardo.ai image fetch ${imgResp.status}`);\n  }\n  \n  const bytes = Buffer.from(await imgResp.arrayBuffer());\n  \n  return {\n    bytes,\n    providerNote: `leonardo.ai/${ctx.model} · ${ctx.aspect} · ${bytes.length} bytes`,\n    suggestedExt: sniffImageExt(bytes),\n  };\n}\n\n","sourceCodeStart":2265,"sourceCodeEnd":2301,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2265-L2301","documentation":"Thrown after the Leonardo polling loop exits (startedAt + maxPollMs = 120000ms = 2 minutes) without imageUrl being set. This is the poll-ceiling timeout: the job neither reached COMPLETE nor FAILED inside the 2-minute window. The message hard-codes '2 minutes' because maxPollMs is a constant.","triggerScenarios":"Loop iterates every 2000ms for at most 120000ms; if generation.status never becomes 'COMPLETE' (and never 'FAILED'), imageUrl stays null and the post-loop guard throws. Happens on slow Leonardo jobs that take >2min or when status gets stuck on PENDING.","commonSituations":"Busy Leonardo queue making jobs run >2min; complex prompt or large size that legitimately needs >2min; status stuck because the poll response shape changed and the COMPLETE branch never matches; maxPollMs is a constant so long jobs always time out.","solutions":["Retry — slow queue days often resolve on resubmit.","Reduce requested complexity (smaller dimensions like 1:1 1024², simpler prompt) so the job finishes inside 2 minutes.","If your jobs consistently need longer, raise the maxPollMs constant (currently 120000) in apps/daemon/src/media/index.ts around line 2250 — consider making it env-configurable like the OpenRouter/Grok ceilings.","If status was returning COMPLETE but imageUrl stayed null (images array empty), that's a different bug — capture pollData to inspect generations_by_pk.generated_images."],"exampleFix":"// before\nconst maxPollMs = 120000; // 2 minutes\n\n// after — env-configurable ceiling matching the OpenRouter/Grok pattern\nconst maxPollMs = Number(process.env.OD_LEONARDO_MAX_POLL_MS) || 120000;","handlingStrategy":"validation","validationCode":"// Make the Leonardo poll ceiling configurable like OpenRouter/Grok so long jobs\n// don't always time out at the 2-minute hard cap.\nconst maxPollMs = Number(process.env.OD_LEONARDO_MAX_POLL_MS) || 120000;","typeGuard":null,"tryCatchPattern":"// Distinguish three post-loop states: status stuck on PENDING (queue backlog),\n// status stuck on a value we don't recognize (parser drift), or COMPLETE with\n// no images (upstream bug). Each needs a different operator action.\nif (!imageUrl) {\n  if (lastStatus === 'COMPLETE') throw new Error('leonardo.ai COMPLETE but no images — upstream bug, file a ticket');\n  throw new Error(`leonardo.ai generation timed out after 2 minutes (last status: ${lastStatus || 'pending'})`);\n}","preventionTips":["Make the Leonardo ceiling env-configurable (OD_LEONARDO_MAX_POLL_MS) to match the pattern already used for OpenRouter and Grok.","Track lastStatus at timeout — 'PENDING' means queue, anything else likely means parser drift on a new status string.","Avoid hardcoded ceilings in new providers; reach for env-config from the start."],"tags":["leonardo","polling-timeout","configuration","hardcoded-ceiling"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}