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
| Row | What it is | Its status is… |
|---|---|---|
| Chat row | the user-facing workspace (reflex_chats, has a title, source='dashboard') | the dashboard status the UI renders |
| Job row | the spawned fine-tuning job — also a reflex_chats row (title=NULL, source='api'), referenced by the chat’s latestJobId | the backend/internal status |
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
| # | Step | Dashboard | Internal (job) | OpenAI-compat | What the user sees |
|---|---|---|---|---|---|
| 1 | chatting | chatting | — | — | the conversation / building the dataset |
| 2 | generating | generating | preparing / generating / labeling | validating_files | ”Generating… / Relabeling…” progress |
| 3 | pending approval | chatting ⚠️ | prepared | queued | the review grid — approve / edit the rows |
| 4 | queued | queued | queued | queued | ”queued to train” |
| 5 | training | training | training | running | training progress |
| 6 | ready | ready | ready | succeeded | model card / playground |
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+ no active training-queue row + review-gated (auto_train:false, not approved) +getJobDatasethas rows ⇒ step 3 (prepared) ⇒ dashboardchatting(review grid). An active queue row ⇒ step 4 (queued/training).
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.