{"record":{"id":"4c5558190c8462a5","repo":"jackwener/OpenCLI","slug":"midjourney-job-jobid-ended-with-status-lasts","errorCode":null,"errorMessage":"Midjourney job ${jobId} ended with status \"${lastStatus}\"","messagePattern":"Midjourney job (.+?) ended with status \"(.+?)\"","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/midjourney/utils.js","lineNumber":521,"sourceCode":"  if (candidates.length > 1) {\n    throw new CommandExecutionError(\n      `Midjourney action is ambiguous; ${candidates.length} derived jobs matched parent ${parentJobId}`,\n      candidates.map((row) => jobUrl(row.id)).join(', '),\n    );\n  }\n  throw new TimeoutError('Midjourney derived job submission', timeoutSeconds, `No new child job appeared for ${parentJobId}.`);\n}\n\nexport async function waitForCompletedJob(page, jobId, timeoutSeconds) {\n  const deadline = Date.now() + timeoutSeconds * 1000;\n  let lastStatus = 'unknown';\n  do {\n    const job = await fetchJobStatus(page, jobId, { allowMissing: true });\n    if (job) {\n      lastStatus = String(job.current_status || job.status || 'unknown').toLowerCase();\n      if (lastStatus === 'completed') return job;\n      if (['failed', 'cancelled', 'canceled', 'error'].includes(lastStatus)) {\n        throw new CommandExecutionError(`Midjourney job ${jobId} ended with status \"${lastStatus}\"`);\n      }\n    }\n    if (!(await waitForNextPoll(page, deadline, 2))) break;\n  } while (true);\n  throw new TimeoutError(\n    `Midjourney job ${jobId} (last status: ${lastStatus})`,\n    timeoutSeconds,\n    `Check the job at ${jobUrl(jobId)} and retry status or download later.`,\n  );\n}\n\nasync function fetchMediaThroughPage(page, url, expectedMimePrefix) {\n  const transferKey = `opencli_media_${Date.now()}_${Math.random().toString(36).slice(2)}`;\n  try {\n    let payload;\n    try {\n      payload = unwrapEvaluateResult(await page.evaluate(async (mediaUrl, key) => {\n        // CDN is public but Cloudflare-protected. Browser-origin fetch succeeds","sourceCodeStart":503,"sourceCodeEnd":539,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/midjourney/utils.js#L503-L539","documentation":"waitForCompletedJob polls the job until it reaches 'completed'. If the job instead lands in a terminal failure state ('failed', 'cancelled', 'canceled', 'error'), the loop stops and throws CommandExecutionError reporting the job ID and final status. This surfaces Midjourney-side job failure promptly rather than waiting for timeout.","triggerScenarios":"The awaited job transitions to failed/cancelled/canceled/error on Midjourney's side: content-policy rejection, moderation block, user cancelled via web UI, or an internal generation error.","commonSituations":"Prompts violating Midjourney's content policy get auto-failed; users cancelling jobs from the web while the CLI waits; Midjourney service errors failing a batch of jobs; jobs failing after parameter errors (bad aspect/seed combos).","solutions":["Read lastStatus in the message and inspect the job on midjourney.com for the failure reason.","If the prompt was moderated/rejected, revise the prompt and submit a new job.","If cancelled, resubmit — do not wait on the old ID.","For transient 'error' status, resubmit the same job/prompt; these often succeed on retry."],"exampleFix":"// before\nconst job = await waitForCompletedJob(page, jobId, 120);\n// after\nlet job;\ntry {\n  job = await waitForCompletedJob(page, jobId, 120);\n} catch (e) {\n  if (/ended with status \"(failed|error)\"/.test(String(e.message))) {\n    job = await resubmitAndWait(page, originalPrompt); // transient failure: resubmit\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// check the job's current status before entering a long wait\nconst job = await fetchJobStatus(page, jobId, { allowMissing: true });\nconst st = String(job?.current_status || job?.status || '').toLowerCase();\nif (['failed', 'cancelled', 'canceled', 'error'].includes(st)) {\n  console.warn(`Job ${jobId} is already ${st}; skipping wait.`);\n}","typeGuard":"function isTerminalFailure(job) {\n  const st = String(job?.current_status || job?.status || '').toLowerCase();\n  return ['failed', 'cancelled', 'canceled', 'error'].includes(st);\n}","tryCatchPattern":"try {\n  const job = await waitForCompletedJob(page, jobId, 120);\n} catch (e) {\n  const m = String(e.message).match(/ended with status \"(\\w+)\"/);\n  if (m && ['failed', 'error'].includes(m[1])) {\n    // transient failure: resubmit the prompt as a new job\n  } else if (m && ['cancelled', 'canceled'].includes(m[1])) {\n    // user- or system-cancelled: do not retry, surface to the user\n  } else throw e;\n}","preventionTips":["Review prompts against Midjourney content policy before submission to avoid moderation failures","Do not cancel jobs in the web UI while CLI automation waits on them","Persist final job status so waits are skipped for known-terminal jobs","Treat 'error' as retryable and 'failed'/'cancelled' as not, when automating retries"],"tags":["midjourney","job-failed","polling"],"backgroundTag":"job-terminated-with-failure","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}