POST /api/index-url Body: ``{repoUrl, repoId?, token?, ref?, zipball?, reindex?}``. Spawns a background thread that clones the repo, runs the indexing pipeline, and reopens the live store after the atomic swap. Returns the job id immediately; the UI polls ``
(request: Request)
| 489 | _trim_index_jobs() |
| 490 | |
| 491 | async def index_url(request: Request) -> JSONResponse: |
| 492 | """POST /api/index-url |
| 493 | |
| 494 | Body: ``{repoUrl, repoId?, token?, ref?, zipball?, reindex?}``. |
| 495 | |
| 496 | Spawns a background thread that clones the repo, runs the |
| 497 | indexing pipeline, and reopens the live store after the atomic |
| 498 | swap. Returns the job id immediately; the UI polls |
| 499 | ``GET /api/index-progress/{jobId}`` for progress. |
| 500 | |
| 501 | Refuses with 409 Conflict when another index is already |
| 502 | running — the underlying pipeline holds an exclusive flock on |
| 503 | the DB, so a concurrent submission would fail anyway, but |
| 504 | only several stages in. Failing fast at the HTTP boundary |
| 505 | lets the UI surface a friendly message (Fix #12). |
| 506 | """ |
| 507 | if db_path is None: |
| 508 | return _error(503, "Server was started without --db; indexing is unavailable") |
| 509 | body = await _read_json(request) |
| 510 | if not body or not body.get("repoUrl"): |
| 511 | return _error(400, "Missing required field: repoUrl") |
| 512 | if not isinstance(body.get("repoUrl"), str): |
| 513 | return _error(400, "Invalid field: repoUrl must be a string") |
| 514 | |
| 515 | # Atomic check-and-insert: hold the lock across both the active-job |
| 516 | # guard and the new job's insertion. Releasing between the two would |
| 517 | # let two concurrent requests both observe no active job and each |
| 518 | # spawn an indexing thread (TOCTOU), wasting clone/parse work. |
| 519 | job_id = uuid.uuid4().hex |
| 520 | with index_jobs_lock: |
| 521 | active = [jid for jid, st in index_jobs.items() if not st.get("done")] |
| 522 | if active: |
| 523 | return _error( |
| 524 | 409, |
| 525 | "Another index is already running on this agent. Wait for it to finish before starting a new one.", |
| 526 | ) |
| 527 | |
| 528 | index_jobs[job_id] = { |
| 529 | "jobId": job_id, |
| 530 | "phase": "queued", |
| 531 | "message": "Queued", |
| 532 | "done": False, |
| 533 | "error": None, |
| 534 | "result": None, |
| 535 | "startedAt": time.time(), |
| 536 | } |
| 537 | |
| 538 | # daemon=True so the thread doesn't keep the process alive past |
| 539 | # uvicorn shutdown (matters for cleanup; not for correctness). |
| 540 | thread = threading.Thread( |
| 541 | target=_run_index_url_job, |
| 542 | args=(job_id, body, db_path), |
| 543 | daemon=True, |
| 544 | name=f"index-url-{job_id[:8]}", |
| 545 | ) |
| 546 | thread.start() |
| 547 | return JSONResponse({"jobId": job_id, "status": "queued"}, status_code=202) |
| 548 |
nothing calls this directly
no test coverage detected