Reflex status flow (read this before touching status code)
Reflex follows OpenAI fine-tuning conventions, so a job’s internal status is not the same as the OpenAI-compatible value the API returns — and there are two rows per workspace. Those two facts cause ~all of the status confusion. Keep both in your head.Two rows per workspace
The UI only ever renders the chat row. So a
status like prepared shows up only on a job row — the user never sees it. (Gotcha: reflex_training_queue.chat_id stores the job uuid, not the chat id.)
Three status vocabularies
- Dashboard (chat row, what the UI keys off):
chatting · generating · queued · training · ready · error - Internal / backend (job row):
queued · preparing · generating · labeling · prepared · training · ready · error · stopped - OpenAI-compatible (what
GET /v1/fine_tuning/jobs/{id}returns):validating_files · queued · running · succeeded · failed · cancelled
The intended lifecycle
error / stopped(→cancelled) are terminal off-paths from any active step.
The two lossy collapses (this is the trap)
tab/reflex api/wire.py::map_reflex_status maps internal → OpenAI-compatible, and it’s lossy:
preparing / generating / labeling→validating_files(can’t tell which)prepared→queued(so step 3 and step 4 both readqueued)training → running,ready → succeeded,error → failed,stopped → cancelled
queued means either “prepared / pending approval” (step 3) or “queued to train” (step 4), the dashboard can’t trust the raw OpenAI value. The seam that disambiguates is src/lib/reflex/job-status.ts::resolveJobChatStatus:
OpenAIEvery status writer must go throughqueued+ review-gated (auto_train:false, not approved) + a draft (orgetJobDatasetrows) ⇒ step 3 (prepared) ⇒ dashboardchatting(review grid). A queue row counts as step 4 only when it is actuallytraining, or when it isqueued/pendingon an approved / auto_train chat. A merely-queuedrow on an unapproved chat is NOT the training line — the backend pre-creates the queue slot at job creation for supplied-training_datajobs (labeled uploads) and it idlesqueueduntil Approve & Train; reading it as “past review” froze upload grids at a phantom “queued” (chat 3f39a4f5). The #390 approve-race (train enqueued, wrapper’s approved write lost) is repaired forward by the/trainroute’s 409 handler instead.
resolveJobChatStatus, not jobStatusToChatStatus directly — bypassing it caused the #390 and #418 regressions.
Known smell: “pending approval” is not a first-class status
Step 3 isn’t its own dashboard status — it’s inferred aschatting + an unapproved prepared dataset. So chatting is overloaded: it means both “just talking” and “review-ready.” This works today (don’t rush to change it), but it’s why the review step is easy to mistake for “still going.” If we ever make it first-class, the seam is already there: check_status returns an explicit phase: 'awaiting_review'.
label_data (relabel) notes
- Teacher-labeling runs through the OpenAI Batch API (
completion_window: "24h",api/openai_batch.py) — minutes to hours, no synchronous path. That’s why fresh relabels are slow to test. - A relabel reaches step 3 correctly (chat
chatting, jobprepared). The review grid for relabel is wired viaLabelDataCard(it was previously only wired forgenerate_data/map_upload), andcheck_statusreports a clear phase instead of leaking the raw OpenAIqueued+ phantom epochs.