jackwener/OpenCLI · error · CommandExecutionError

Midjourney job ${jobId} ended with status "${lastStatus}"

Error message

Midjourney job ${jobId} ended with status "${lastStatus}"

What it means

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.

Source

Thrown at clis/midjourney/utils.js:521

  if (candidates.length > 1) {
    throw new CommandExecutionError(
      `Midjourney action is ambiguous; ${candidates.length} derived jobs matched parent ${parentJobId}`,
      candidates.map((row) => jobUrl(row.id)).join(', '),
    );
  }
  throw new TimeoutError('Midjourney derived job submission', timeoutSeconds, `No new child job appeared for ${parentJobId}.`);
}

export async function waitForCompletedJob(page, jobId, timeoutSeconds) {
  const deadline = Date.now() + timeoutSeconds * 1000;
  let lastStatus = 'unknown';
  do {
    const job = await fetchJobStatus(page, jobId, { allowMissing: true });
    if (job) {
      lastStatus = String(job.current_status || job.status || 'unknown').toLowerCase();
      if (lastStatus === 'completed') return job;
      if (['failed', 'cancelled', 'canceled', 'error'].includes(lastStatus)) {
        throw new CommandExecutionError(`Midjourney job ${jobId} ended with status "${lastStatus}"`);
      }
    }
    if (!(await waitForNextPoll(page, deadline, 2))) break;
  } while (true);
  throw new TimeoutError(
    `Midjourney job ${jobId} (last status: ${lastStatus})`,
    timeoutSeconds,
    `Check the job at ${jobUrl(jobId)} and retry status or download later.`,
  );
}

async function fetchMediaThroughPage(page, url, expectedMimePrefix) {
  const transferKey = `opencli_media_${Date.now()}_${Math.random().toString(36).slice(2)}`;
  try {
    let payload;
    try {
      payload = unwrapEvaluateResult(await page.evaluate(async (mediaUrl, key) => {
        // CDN is public but Cloudflare-protected. Browser-origin fetch succeeds

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read lastStatus in the message and inspect the job on midjourney.com for the failure reason.
  2. If the prompt was moderated/rejected, revise the prompt and submit a new job.
  3. If cancelled, resubmit — do not wait on the old ID.
  4. For transient 'error' status, resubmit the same job/prompt; these often succeed on retry.

Example fix

// before
const job = await waitForCompletedJob(page, jobId, 120);
// after
let job;
try {
  job = await waitForCompletedJob(page, jobId, 120);
} catch (e) {
  if (/ended with status "(failed|error)"/.test(String(e.message))) {
    job = await resubmitAndWait(page, originalPrompt); // transient failure: resubmit
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check the job's current status before entering a long wait
const job = await fetchJobStatus(page, jobId, { allowMissing: true });
const st = String(job?.current_status || job?.status || '').toLowerCase();
if (['failed', 'cancelled', 'canceled', 'error'].includes(st)) {
  console.warn(`Job ${jobId} is already ${st}; skipping wait.`);
}

Type guard

function isTerminalFailure(job) {
  const st = String(job?.current_status || job?.status || '').toLowerCase();
  return ['failed', 'cancelled', 'canceled', 'error'].includes(st);
}

Try / catch

try {
  const job = await waitForCompletedJob(page, jobId, 120);
} catch (e) {
  const m = String(e.message).match(/ended with status "(\w+)"/);
  if (m && ['failed', 'error'].includes(m[1])) {
    // transient failure: resubmit the prompt as a new job
  } else if (m && ['cancelled', 'canceled'].includes(m[1])) {
    // user- or system-cancelled: do not retry, surface to the user
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4c5558190c8462a5. Report an issue: GitHub.