# Async Job Polling Guide

Question-asking and study-completion operations are asynchronous.
These endpoints return queued job IDs instead of immediate final results.

## Endpoints That Queue Jobs

- `POST /v1/research-agents/{agent_id}/questions`
- `POST /v1/research-groups/{group_id}/questions`
- `POST /v1/research-studies/{study_id}/questions`
- `POST /v1/research-studies/{study_id}/complete`
- `POST /v1/free/questions`
- `POST /v1/research-groups:from-description`
- `POST /v1/research-groups/{group_id}:ask`
- `POST /v1/thesis:compose-and-recruit`

Poll status with:

- `GET /v1/jobs/{job_id}`

Request create endpoints are intentionally excluded here.
`POST /v1/research-group-requests` and `POST /v1/research-study-requests`
use request-specific status routes as their primary follow-up paths:
`GET /v1/research-group-requests/{request_id}` and
`GET /v1/research-study-requests/{request_id}`.
Only fall back to `GET /v1/jobs/{job_id}` when you truly only have the queued
job id and no request id.

## Standard Polling Pattern

1. Submit a question/completion request and store one returned `job_id`.
   For direct agent/group questions, also store the returned `question_id` or
   `question_uuid`.
2. Wait before first poll (recommended: ~45-50 seconds for question jobs).
3. Call `GET /v1/jobs/{job_id}`.
4. If `status` is `started`, wait ~20 seconds and poll again.
5. If `status` is `finished`, parse `result` and continue workflow.
   For direct agent/group questions, follow with
   `GET /v1/direct-questions/{question_id}` to retrieve the canonical saved
   record, normalized answers, and any summary artifact. Use
   `GET /v1/direct-questions` when you need to browse recent saved direct
   questions across the organization.
6. If `status` is `failed`, inspect `error` and stop/retry appropriately.
7. If `status` polling returns `429`, wait for the `Retry-After` interval
   before polling again. This is especially relevant for free-tier keys.

## One `job_id` Is Usually Enough

Study and group question endpoints can return multiple `job_ids`.
For jobs created by the same question request, polling one `job_id` is usually sufficient to track completion state.
If your workflow requires strict per-agent confirmation, poll each `job_id`.

## One-Call Async Pattern

The Thesis Lab golden-path endpoints use a consistent one-call async contract:

1. Send the create/ask request with an `Idempotency-Key` header.
2. The API reserves quota and returns `202 Accepted` with `job.id`,
   `job.poll_url`, `operation.id`, and `usage_recorded[]`.
   `operation.id` is the canonical operation UUID accepted by
   `GET /v1/organization/operations/{operation_id}`.
3. Persist the `operation.id`, `job.id`, and original idempotency key.
4. Poll `GET /v1/jobs/{job_id}` until the job finishes or fails.
   For Thesis Lab and request-backed jobs, read `domain_status` to understand
   the linked domain resource. The generic `status` field is still the queued
   job status.
5. Retry the original request with the same idempotency key only when you need
   to recover from a client/network failure. The same body replays the existing
   operation; a different body returns `409 idempotency_key_conflict`.

`wait_seconds` is accepted on golden-path request bodies but is capped at 60
seconds. It controls how long the REST route may wait before returning the async
handle; it does not change the job timeout. MCP tools have a separate optional
`poll_timeout_seconds` argument for wrapper-side polling.

The `usage_recorded[]` array is the authoritative quota/billing trace emitted
at acceptance time. For compose-and-recruit, thesis/group usage is initially
`reserved` while the async Thesis Lab runtime executes; follow-on settlement
commits delivered usage and voids undelivered reserved usage.

## Response Shapes

Example (still running):

```json
{
  "status": "started",
  "result": null,
  "error": null,
  "meta": {}
}
```

Example (finished):

```json
{
  "status": "finished",
  "domain_status": "completed",
  "result": {},
  "error": null,
  "meta": {}
}
```

Example (finished job whose linked domain resource still needs customer action):

```json
{
  "status": "finished",
  "domain_status": "needs_clarification",
  "operation_id": "op_7bbf8c71",
  "operation_url": "/v1/organization/operations/op_7bbf8c71",
  "resource_url": "/v1/thesis/37/state",
  "result": {},
  "meta": {}
}
```

`domain_status` is additive. Existing clients that only inspect `status` can
continue doing so, but Thesis Lab clients should prefer `domain_status` when it
is present because a finished queue job can still correspond to a domain state
such as `needs_clarification`, `awaiting_review`, `completed`, or `failed`.

## Workflow Guardrails

- Ask study questions sequentially: queue one question, wait for finish, then ask the next.
- Do not call study completion until question jobs have finished.
- Persist `job_id`, endpoint context, and submitted question text for traceability.
- For direct question endpoints, persist `question_id` / `question_uuid` so the
  saved question can be fetched after the async job completes.
- Free-tier clients should treat `Retry-After` as authoritative on both ask and
  polling `429` responses.
