{"record":{"id":"ce3ac806262936dc","repo":"nexu-io/open-design","slug":"grok-video-timed-out-after-elapsedsec-s-waiting","errorCode":null,"errorMessage":"grok video timed out after ${elapsedSec}s waiting for status=done (last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). If your jobs legitimately need longer, raise OD_GROK_VIDEO_MAX_POLL_MS.","messagePattern":"grok video timed out after (.+?)s waiting for status=done \\(last status: (.+?), ceiling (.+?)s\\)\\. If your jobs legitimately need longer, raise OD_GROK_VIDEO_MAX_POLL_MS\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2406,"sourceCode":"      }\n      if (lastStatus === 'done' || lastStatus === 'succeeded') {\n        videoUrl = pollData?.video?.url || null;\n        break;\n      }\n      if (lastStatus === 'failed' || lastStatus === 'expired') {\n        const reasonRaw = pollData?.error?.message || pollData?.error || lastStatus;\n        const reason = typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);\n        throw new Error(`grok task ${lastStatus}: ${reason}`);\n      }\n    }\n    // Loop exited without a videoUrl. Distinguish the two reachable\n    // cases so operators know which lever to pull: bumping the poll\n    // ceiling (timeout) vs filing a bug against the upstream contract\n    // (status=done but no video.url).\n    if (!videoUrl) {\n      const elapsedSec = Math.round((Date.now() - startedAt) / 1000);\n      const ceilingSec = Math.round(maxMs / 1000);\n      throw new Error(\n        `grok video timed out after ${elapsedSec}s waiting for status=done `\n        + `(last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). `\n        + `If your jobs legitimately need longer, raise OD_GROK_VIDEO_MAX_POLL_MS.`,\n      );\n    }\n  }\n\n  if (!videoUrl) {\n    // Submit returned neither an inline video.url nor a request_id —\n    // upstream broke its own contract. Surfacing the last status helps\n    // pinpoint whether it was a transient API blip or a malformed\n    // response we should add a parser branch for.\n    throw new Error(\n      `grok video submit returned no inline video and no request_id to poll `\n      + `(status=${lastStatus || 'unknown'})`,\n    );\n  }\n","sourceCodeStart":2388,"sourceCodeEnd":2424,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2388-L2424","documentation":"Thrown when the Grok polling loop exits its window (startedAt + maxMs) without videoUrl being set and without hitting a failed/expired terminal status. maxMs defaults per mode (i2v vs t2v) and is env-overridable via OD_GROK_VIDEO_MAX_POLL_MS. The message reports elapsed seconds, last observed status, and the ceiling so the operator can decide whether to raise the limit. Distinct from [476] (definitive failure) and [478] (no id to poll at all).","triggerScenarios":"Grok video generation where status stays pending/processing inside the poll loop for longer than maxMs (default mode-dependent, capped). Triggered when the while-loop exits with videoUrl still null and lastStatus never reached 'done'/'succeeded' nor 'failed'/'expired'.","commonSituations":"Long Grok video jobs that legitimately exceed the default ceiling; xAI queue backlog; default ceiling too low for the requested duration/complexity; status string xAI returns doesn't match the 'done'/'succeeded' matcher so the loop never breaks.","solutions":["Raise the ceiling: set OD_GROK_VIDEO_MAX_POLL_MS higher in the daemon environment if your jobs legitimately run long.","Reduce requested duration or pick a faster path (t2v vs i2v) so xAI finishes within the default window.","Check xAI status/dashboard — confirm the job actually stalled vs. is still processing; retry if it was queue backlog.","If lastStatus shows a value that isn't 'done'/'succeeded' but also isn't failed/expired, inspect whether xAI added a new success status the parser should treat as complete."],"exampleFix":"// before\n// OD_GROK_VIDEO_MAX_POLL_MS unset → default ceiling per mode\n\n// after (operator)\nexport OD_GROK_VIDEO_MAX_POLL_MS=1800000\n# restart the daemon so the env var is picked up","handlingStrategy":"retry","validationCode":"// Before invoking renderGrokVideo, sanity-check the configured ceiling against\n// the requested job size so callers get a fast config error instead of a long\n// timeout.\nfunction assertGrokCeilingOk(mode: 't2v' | 'i2v', requestedLengthSec: number) {\n  const maxMs = Number(process.env.OD_GROK_VIDEO_MAX_POLL_MS)\n    || (mode === 'i2v' ? 12 * 60 * 1000 : 8 * 60 * 1000);\n  const estimatedMs = requestedLengthSec * 45 * 1000; // ~45s/s, conservative for Grok\n  if (estimatedMs > maxMs) {\n    throw new Error(\n      `requested ${requestedLengthSec}s grok ${mode} likely exceeds OD_GROK_VIDEO_MAX_POLL_MS (${Math.round(maxMs / 1000)}s); raise the env var or shorten the request.`,\n    );\n  }\n}","typeGuard":null,"tryCatchPattern":"// Catch the timeout specifically and surface the raise-the-ceiling hint to the user.\ntry {\n  await renderGrokVideo(ctx, creds, onProgress);\n} catch (e) {\n  const msg = String((e as Error).message || e);\n  if (msg.startsWith('grok video timed out')) {\n    throw new Error(`Grok video generation timed out. Ask the operator to raise OD_GROK_VIDEO_MAX_POLL_MS. Detail: ${msg}`);\n  }\n  throw e;\n}","preventionTips":["Set OD_GROK_VIDEO_MAX_POLL_MS explicitly based on your typical job length; do not rely on the per-mode default.","Track p95 job duration per mode in observability and alert before it approaches the ceiling.","If lastStatus at timeout is a value other than pending/done/succeeded/failed/expired, suspect parser drift on a new xAI status string."],"tags":["grok","xai","video-generation","polling-timeout","configuration"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}