abhigyanpatwari/GitNexus · warning

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

Error message

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

What it means

HTTP 409 returned by POST /api/analyze when JobManager.createJob throws `Analysis already in progress (job <uuid>)` (analyze-job.ts:114) — its single-slot rule: the manager runs one active job at a time, and a non-terminal job for a DIFFERENT repository already exists (queued/cloning/analyzing). The route maps any message containing 'already in progress' to 409. The same-repo case is not an error: createJob dedupes it and the route answers 202 with the existing jobId (plus a tokenIgnored note). /api/analyze/upload shares the same manager, so an upload analysis also occupies the slot.

Source

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

            if (!targetPath) {
              throw new Error('No target path resolved');
            }

            launchAnalysisWorker(job, targetPath, { force, embeddings, dropEmbeddings });
          } catch (err: any) {
            if (targetPath) releaseRepoLock(getStoragePath(targetPath));
            jobManager.updateJob(job.id, {
              status: 'failed',
              error: err.message || 'Analysis failed',
            });
          }
        })();

        res.status(202).json({ jobId: job.id, status: job.status });
      } catch (err: any) {
        if (err.message?.includes('already in progress')) {
          res.status(409).json({ error: err.message });
        } else {
          res.status(500).json({ error: err.message || 'Failed to start analysis' });
        }
      }
    },
  );

  // POST /api/analyze/upload — analyze a browser folder upload.
  // Securely ingests the multipart upload into a sandbox, promotes it to a
  // persistent dir, and analyzes it via the shared job/worker machinery.
  // localhost-only (no cross-origin write reach) + conservative rate limit.
  app.post(
    '/api/analyze/upload',
    createRouteLimiter({ limit: 5 }),
    requireTrustedOrigin,
    createAnalyzeUploadHandler({
      createJob: (params) => jobManager.createJob(params),
      launch: (job, targetPath, opts) => launchAnalysisWorker(job, targetPath, opts),

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Adopt the running job: parse its id from the 409 message (`job <uuid>`) and poll GET /api/analyze/:jobId (or its /progress SSE stream) instead of creating a new one
  2. Serialize client-side — the manager allows exactly one active analysis, so queue repos and start the next only after the previous reaches complete/failed
  3. Re-posting the SAME repo is safe: you get a 202 dedup with the existing id, not a 409
  4. If the slot seems stuck, poll the active job — a hung worker eventually times out (30-minute job timeout) and frees the slot

Example fix

// before
const res = await fetch('/api/analyze', { method: 'POST' /* ... */ });
if (!res.ok) throw new Error(await res.text()); // 409 crashes the flow

// after
const res = await fetch('/api/analyze', { method: 'POST' /* ... */ });
if (res.status === 409) {
  const { error } = await res.json();
  const existingId = error.match(/job ([0-9a-f-]{36})/)?.[1];
  if (existingId) await pollUntilTerminal(`/api/analyze/${existingId}`); // wait it out
} else if (!res.ok) throw new Error((await res.json()).error);
Defensive patterns

Strategy: fallback

Validate before calling

// Single-flight: concurrent callers reuse one POST instead of colliding
let inflight: Promise<Response> | null = null;
async function postAnalyze(body: object): Promise<Response> {
  if (inflight) return inflight;
  const p = fetch('/api/analyze', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  inflight = p;
  try { return await p; } finally { if (inflight === p) inflight = null; }
}

Try / catch

Catch HTTP 409 specifically, extract the active job id from `job <uuid>` in the message, and fall back to polling that job (GET /api/analyze/:jobId or the SSE stream). Do not retry the POST unchanged while the other job is active — it will keep conflicting.

Prevention

When it happens

Trigger: POST /api/analyze for repo B while repo A's analysis (or upload analysis) is still running; a retry double-click racing a slow clone; batch scripts iterating repos without waiting for terminal status; UIs firing a second POST before polling the first.

Common situations: Treating the 202 as completion and re-POSTing; parallel CI jobs against one serve instance; dashboards queuing multiple repos simultaneously; very large clones holding the single slot for a long time.

Related errors


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