abhigyanpatwari/GitNexus · error · BadRequestError

Analysis already in progress (job ${job.id})

Error message

Analysis already in progress (job ${job.id})

What it means

Inside POST /api/analyze/upload, deps.createJob (the same single-slot JobManager behind error 244) threw 'Analysis already in progress (job ...)' because a non-terminal job for a different target holds the slot. The upload handler catches it and rethrows BadRequestError(msg, 409) so HTTP callers see a correct conflict status rather than a generic 500.

Source

Thrown at gitnexus/src/server/analyze-upload.ts:111

        innerIsDir = false;
      }
      if (!innerIsDir) {
        throw new BadRequestError('Upload must be a folder');
      }

      const finalName = await pickAvailableName(baseName);
      const finalDir = getUploadDir(finalName);

      // createJob occupies the single analysis slot (throws 'already in
      // progress' → 409). From here on, ANY error before launch MUST release
      // the slot via failJob in the catch, or the server wedges all analyses.
      let job: UploadJobRef;
      try {
        job = deps.createJob({ repoPath: finalDir });
      } catch (err) {
        const msg = err instanceof Error ? err.message : '';
        if (msg.includes('already in progress')) {
          throw new BadRequestError(msg, 409);
        }
        throw err;
      }
      createdJobId = job.id;

      // Promote staging → persistent upload dir. Both live under UPLOAD_ROOT's
      // filesystem, so this rename stays atomic (no EXDEV).
      await fsp.mkdir(UPLOAD_ROOT, { recursive: true });
      await fsp.rename(innerRoot, finalDir);
      promotedDir = finalDir;
      const oldStage = stageRoot;
      stageRoot = undefined;
      await fsp.rm(oldStage, { recursive: true, force: true }).catch(() => {});

      // Drop any crafted index the upload may have carried (a `.gitnexus`
      // segment passes containment); the worker will build a fresh one.
      await fsp
        .rm(path.join(finalDir, '.gitnexus'), { recursive: true, force: true })

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Poll the active job's status until terminal, then re-upload
  2. Cancel the in-flight job if it is no longer wanted
  3. Disable/queue the upload action in the UI while a job is active

Example fix

// before
const res = await fetch('/api/analyze/upload', { method: 'POST', body: formData });
if (res.status === 409) throw new Error('upload failed');

// after
if (res.status === 409) {
  await waitForTerminalJob(); // poll job status
  return fetch('/api/analyze/upload', { method: 'POST', body: formData });
}
return res;
Defensive patterns

Strategy: retry

Validate before calling

async function slotFree(getActiveJob, isTerminal) {
  const j = await getActiveJob();
  return !j || isTerminal(j.status);
}

Type guard

function isConflictResponse(res) { return res.status === 409; }

Try / catch

async function uploadWithRetry(formData, { intervalMs = 5000, maxMs = 30 * 60 * 1000 } = {}) {
  const deadline = Date.now() + maxMs;
  for (;;) {
    const res = await fetch('/api/analyze/upload', { method: 'POST', body: formData });
    if (res.status !== 409) return res;
    if (Date.now() > deadline) throw new Error('analysis slot still busy');
    await sleep(intervalMs);
  }
}

Prevention

When it happens

Trigger: POST /api/analyze/upload while any other analysis — git-URL clone, local-path analyze, or a previous upload — is in a non-terminal status.

Common situations: Two browser tabs uploading folders concurrently; an upload fired while a long clone is running; UI not disabling the upload button during an active job.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/944ed3c61e961212. Report an issue: GitHub.