abhigyanpatwari/GitNexus · warning

Job already ${job.status}

Error message

Job already ${job.status}

What it means

HTTP 400 returned by DELETE /api/analyze/:jobId when the job exists but is already terminal — isTerminalJobStatus covers 'complete' and 'failed' only. Terminal outcomes are immutable by design (updateJob drops later transitions so a racing complete/error cannot flip a result), so there is nothing left to cancel. A successful cancel itself sets status 'failed' with error 'Cancelled by user'.

Source

Thrown at gitnexus/src/server/api.ts:1704

      error: job.error,
      startedAt: job.startedAt,
      completedAt: job.completedAt,
    });
  });

  // GET /api/analyze/:jobId/progress — SSE stream (shared helper)
  mountSSEProgress(app, '/api/analyze/:jobId/progress', jobManager);

  // DELETE /api/analyze/:jobId — cancel a running analysis job
  app.delete('/api/analyze/:jobId', requireTrustedOrigin, (req, res) => {
    const jobId = req.params.jobId as string;
    const job = jobManager.getJob(jobId);
    if (!job) {
      res.status(404).json({ error: 'Job not found' });
      return;
    }
    if (isTerminalJobStatus(job.status)) {
      res.status(400).json({ error: `Job already ${job.status}` });
      return;
    }
    jobManager.cancelJob(jobId, 'Cancelled by user');
    res.json({ id: job.id, status: 'failed', error: 'Cancelled by user' });
  });

  // ── Embedding endpoints ────────────────────────────────────────────

  const embedJobManager = new JobManager();

  // POST /api/embed — trigger server-side embedding generation
  app.post(
    '/api/embed',
    createRouteLimiter({ limit: 20 }),
    requireTrustedOrigin,
    async (req, res) => {
      try {
        const entry = await resolveRepo(requestedRepo(req));

View on GitHub (pinned to 0d1aed942f)

Solutions

  1. Read the status out of the message ('Job already complete' / 'Job already failed') and treat the job as finished — POST /api/analyze again if you want a fresh run
  2. Guard before cancelling: GET /api/analyze/:jobId first and only DELETE when status is non-terminal
  3. Disable cancel controls in the UI once status is complete/failed
  4. Remember cancel = forced 'failed'; don't re-cancel an already-cancelled job

Example fix

// before
await fetch(`/api/analyze/${jobId}`, { method: 'DELETE' }); // 400 once terminal

// after
const job = await getJson(`/api/analyze/${jobId}`);
if (job.status !== 'complete' && job.status !== 'failed') {
  await fetch(`/api/analyze/${jobId}`, { method: 'DELETE' });
}
Defensive patterns

Strategy: validation

Validate before calling

// Only cancel live jobs
const { status } = await getJson(`/api/analyze/${jobId}`);
const live = status !== 'complete' && status !== 'failed';
if (live) await fetch(`/api/analyze/${jobId}`, { method: 'DELETE' });

Type guard

type JobStatus = 'queued' | 'cloning' | 'analyzing' | 'loading' | 'complete' | 'failed';
const isTerminalJobStatus = (s: JobStatus): boolean => s === 'complete' || s === 'failed';

Try / catch

If the DELETE still returns 400 'Job already …', accept it as the terminal verdict and optionally start a new analysis; do not retry the cancel.

Prevention

When it happens

Trigger: DELETE after the job completed; DELETE after it failed on its own; a second DELETE after the first succeeded (the job is now 'failed', so you get 400 rather than 404 while the record lives); a cancel racing the final status update.

Common situations: UI cancel buttons lagging behind completion; pollers deciding to cancel after observing the terminal state; retry wrappers that cancel-then-restart.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-08-20). Data as JSON: /api/errors/130bd7889951f611. Report an issue: GitHub.