---
url: https://docs.nemo12.com/reference/state-machines.md
description: >-
  State Machines: mọi giá trị enum trạng thái, ai được đổi và đổi theo đường
  nào, mỗi bảng D1 có cột status đều có mục (AS-04.3.5).
---

# State Machines

Mọi giá trị enum trong hệ thống, ai được đổi, và đổi theo đường nào. Trạng thái nằm rải rác trong code là nguồn của lỗi âm thầm — trang này gom về một chỗ.

**Luật của trang này (AS-04.3.5):** mỗi bảng D1 có cột `status` phải có một mục ở đây, và tập giá trị phải khớp **đúng** `CHECK` constraint trong `migrations/`. Lệch một giá trị là lệch tài liệu, không phải "gần đúng".

Hai loại trạng thái, đừng lẫn:

* **Trạng thái lưu** — có cột `status` trong D1, đổi bằng một hành động cụ thể của ai đó. §5 trở đi.
* **Trạng thái suy ra** — không lưu, tính lại mỗi lần đọc từ số liệu model. §1–§4.

***

## Phần A — Trạng thái suy ra (không lưu)

## 1. Skill state — mức vững một node

Suy ra từ `(mastery, confidence)`, **không lưu**, tính bằng `stateFor()`.

```
confidence < 0.25  →  unknown    ⚪ "Đang làm quen"
mastery ≥ 0.8      →  chac       🟢 "Chắc rồi"
mastery ≥ 0.5      →  lung_lay   🟡 "Đang lung lay"
còn lại            →  hong       🔴 "Cần xây lại từ gốc"
```

Confidence **luôn thắng**: chưa đủ bằng chứng thì không được kết luận "hổng". Không có chuyển tiếp trực tiếp — trạng thái đổi khi mastery/confidence đổi.

Màu: đỏ **không** dùng cho trạng thái học tập; `hong` là coral ấm ("cần xây lại"), không phải màu lỗi (REQ-UX-03).

## 2. Need signal — "bài này có cần học không"

Nhãn hiển thị trong Phòng Lab, suy từ skill state:

| Need | Từ state | Nhãn |
| --- | --- | --- |
| `urgent` | `hong` | Cần học |
| `review` | `lung_lay` | Nên ôn |
| `solid` | `chac` | Đã vững |
| `new` | `unknown` | Chưa học |

## 3. Retention group label

Từ `current_retention` (đã decay) + `historical_mastery`:

```
historical_mastery < 0.6  →  null           (chưa từng vững — không thuộc bức tranh trí nhớ)
retention ≥ 0.80          →  dang_chac      Đang chắc
retention ≥ 0.65          →  nhac_lai       Cần nhắc lại sớm
retention ≥ 0.45          →  nguy_co_quen   Có nguy cơ quên
còn lại                   →  nen_on_lai     Nên ôn lại
```

## 4. Review urgency

`NONE → LOW → MEDIUM → HIGH → CRITICAL`. Bắt đầu từ thang retention rồi điều chỉnh theo ngữ cảnh:

| Điều kiện | Dịch chuyển |
| --- | --- |
| `historical_mastery < 0.6` | ép về `NONE` |
| thuộc goal **và** thi ≤14 ngày | **+1 bậc** |
| đang chặn ≥3 bài sau | **+1 bậc** |
| không thuộc goal nào **và** không có kỳ thi | **−1 bậc** (sàn `LOW`) |

Chi tiết: [retention-model §6.1](retention-model.md#61-review-queue--get-v1learnersidretentionreview-queue).

***

## Phần B — Danh tính & phiên

## 5. User

`users.status` — migration 0001.

```
active  ──┬──▶  suspended   (vận hành khoá tạm, đăng nhập lại được sau khi mở)
          └──▶  deleted     (xoá mềm — hàng còn, dữ liệu trỏ tới đã đi qua đường xoá)
```

`deleted` **không** phải xoá cứng: nhật ký truy cập và bản ghi đồng thuận vẫn phải trỏ về một hàng có thật. Xoá thật đi qua `data_deletion_requests` (§13). Ai đổi: chỉ vận hành/admin.

## 6. Session

`sessions` — migration 0001. Bảng **không có cột `status`**; trạng thái suy từ ba cột thời gian, và đó là chủ đích: một phiên chỉ có thể chết theo ba cách khác nhau về nguyên nhân.

```
(đăng nhập)  ──▶  active  ──┬──▶  rotated    (quá 7 ngày → cấp token mới, rotated_from trỏ về bản cũ)
                            ├──▶  expired    (quá expires_at)
                            └──▶  revoked    (revoked_at — đăng xuất/thu hồi)
```

Session hợp lệ = `revoked_at IS NULL` **và** `expires_at > now`. Token lưu dưới dạng **SHA-256 hash**, không bao giờ lưu thô. `client` ∈ `web` | `mobile` quyết định luật CSRF nào áp dụng, không phải trạng thái.

## 7. Invitation

`invitations.status` — migration 0001.

```
pending  ──┬──▶  accepted   (accepted_by + accepted_at được điền)
           ├──▶  expired    (quá expires_at — không ai bấm)
           └──▶  revoked    (người mời rút lại)
```

Ba nhánh kết là **cuối** — không quay lại `pending`. Mời lại là **tạo lời mời mới** với `code` mới, không hồi sinh dòng cũ: mã cũ đã có thể lọt ra ngoài. `role` ∈ `learner` | `guardian` | `supporter` là vai sẽ được cấp khi chấp nhận, không phải trạng thái.

## 8. Learner

`learners.status` — migration 0001.

```
invited  ──▶  active  ──▶  paused  ──▶  active
                     └──▶  archived
```

* `invited` — bố mẹ đã tạo hồ sơ, **con chưa có tài khoản** (`user_id IS NULL`). Đây là trạng thái bình thường, không phải dữ liệu dở dang.
* `active` — đang học.
* `paused` — tạm dừng (nghỉ hè, ốm dài). Dữ liệu giữ nguyên, engine không đẩy nhắc nhở.
* `archived` — không còn dùng. **Không xoá dữ liệu** — xoá đi qua §13.

## 9. School enrollment

`school_enrollments.status` — migration 0003: `active` → `paused` → `left`. Từ `paused` quay lại `active` được; `left` là cuối. Chỉ enrollment `active` mới sinh goal trong Goal Model.

## 10. Mentor assignment

`mentor_assignments.status` — migration 0008: `active` ⇄ `ended`. **Không phải điều kiện truy cập** (SRC-037): mentor xem được mọi learner bất kể có assignment hay không; bảng này chỉ nói ai theo dõi chính. Xem [permissions](permissions.md).

***

## Phần C — Quyền riêng tư & dữ liệu

## 11. Consent

`consents.granted` (0/1) + `granted_at` / `revoked_at` — migration 0033. Không có cột `status`; trạng thái là cặp `granted` + thời điểm.

```
(chưa hỏi: không có dòng)
        │  phụ huynh bấm đồng ý
        ▼
   granted = 1, granted_at = now, policy_version = bản đang hiệu lực
        │  phụ huynh rút lại
        ▼
   granted = 0, revoked_at = now          ──▶  đồng ý lại: granted = 1, granted_at mới
```

Ba luật:

1. **Không có dòng ≠ đã từ chối.** Không có dòng nghĩa là **chưa hỏi** — tính năng của scope đó không được chạy, y như bị từ chối, nhưng giao diện phải hỏi chứ không được báo "bạn đã từ chối".
2. **Rút lại không xoá dòng.** `granted=0` + `revoked_at`; bảng này là bằng chứng pháp lý.
3. **Đổi chính sách không tự động huỷ đồng thuận cũ.** `policy_version` giữ nguyên bản lúc bấm; muốn áp bản mới thì phải hỏi lại.

7 scope: `account`, `learning_data` (bắt buộc — không có thì không có tài khoản), `ai_processing`, `portrait`, `community`, `mentor_access`, `research`.

## 12. Data deletion request

`data_deletion_requests.status` — migration 0033.

```
pending  ──▶  in_progress  ──▶  completed
      └────────────────────▶  rejected    (kèm lý do trong `note`)
```

`completed` bắt buộc điền `note`: **đã xoá gì, giữ lại gì, vì sao**. Một yêu cầu xoá kết thúc mà không nói được đã làm gì thì về mặt bằng chứng là chưa làm.

## 13. Retention policy

`retention_policies` không có trạng thái — là bảng khai báo. Nêu ở đây để trả lời dứt điểm: **không** có vòng đời, sửa là sửa thẳng, lịch sử nằm ở git của migration.

***

## Phần D — Nội dung

## 14. Item — vòng đời một câu hỏi

`items.status` — migration 0032. Đây là trạng thái quan trọng nhất của khu nội dung.

```
candidate  ──┬──▶  published  ──▶  retired
             └──▶  (ở nguyên candidate nếu không đạt ngưỡng chất lượng)
```

| Giá trị | Nghĩa | Learner có thấy không |
| --- | --- | --- |
| `candidate` | Đã sinh/đã sửa, **chờ đạt ngưỡng** đánh giá | ❌ |
| `published` | Đang phục vụ learner | ✅ |
| `retired` | Gỡ khỏi luồng, **giữ lịch sử** để bài đã làm vẫn giải thích được | ❌ |

Mặc định của cột là `published` — có chủ đích, để 2.146 item đang chạy không biến mất khỏi Phòng Lab ngay sau migration. Item mới do AI sinh **luôn** bắt đầu ở `candidate` (AS-10.4.1).

`retired` **không** quay lại `published`: hồi sinh nội dung là tạo version mới và publish version đó (§15).

## 15. Item version — vì sao không sửa trực tiếp

`item_versions.status` — migration 0032.

```
candidate  ──┬──▶  published   ──▶  superseded   (version mới hơn được publish)
             └──▶  rejected                       (người soát bác — giữ lại để biết đã bác gì)
```

* Mỗi thay đổi nội dung **tạo version mới**, không sửa bản đang chạy (AS-06.3.1 🔴).
* **Publish là đổi con trỏ**, không phải ghi đè: đúng một version `published` cho mỗi `item_id`.
* **Rollback = publish lại một version cũ**, không sửa tay DB. Đây là lý do `superseded` phải bất biến.
* `rejected` là nhánh cuối; muốn dùng lại ý tưởng đó thì tạo version mới.

## 16. Content review queue — báo sai thì đi đâu

`content_review_queue.status` — migration 0032.

```
open  ──▶  in_review  ──┬──▶  resolved    (đã sửa — kèm `resolution`)
                        └──▶  dismissed   (không phải lỗi — vẫn kèm `resolution`)
```

Hai nguồn đổ vào: **người báo** (learner/phụ huynh, qua `content_reports`) và **máy sàng lọc** (`items.screen_flags_json`, `reported_by IS NULL`). Cả hai nhánh kết đều **bắt buộc** ghi `resolution` — đó là điều biến "learner báo sai" thành một vòng khép kín thay vì rơi vào hư vô (AS-10.4.5).

## 17. Content report

`content_reports.status` — migration 0006: `open` → `reviewed` | `dismissed` | `actioned`. Là **sổ nhận báo cáo** của khu tương tác; việc xử lý nội dung học đi tiếp sang `content_review_queue` (§16).

## 18. Learning experience

`learning_experiences.status` — migration 0019: `draft` → `review` → `published` → `deprecated`. Experience do AI sinh vào `draft`, **không tới learner** cho tới khi người soát duyệt.

## 19. Content blueprint

`content_blueprints.status` — migration 0016: `draft` → `active` → `deprecated`. Blueprint `deprecated` không sinh nội dung mới nhưng nội dung đã sinh từ nó vẫn sống.

## 20. Generation run

`generation_runs.status` — migration 0016.

```
queued  ──▶  running  ──┬──▶  review  ──▶  done
                        └──▶  failed
```

`review` là trạng thái riêng có chủ đích: lô đã sinh xong về mặt kỹ thuật nhưng **chưa ai nhìn**. Không có `review` thì `done` sẽ nói dối.

## 21. Lab

`labs.status` — migration 0037: `draft` → `live` → `retired`. Mặc định `live`.

## 22. Exam — đề thi

`exams.status` — migration 0004: `draft` → `published` → `archived`.

**Đề thi thật đã publish là bất biến** (QG-011): sửa nội dung đề thật = tạo đề mới, không sửa tại chỗ. `source` phân biệt `generated` (hệ thống soạn) với đề có nguồn thật; `archived` giữ để bài làm cũ vẫn chấm lại được.

## 23. Exam attempt & assessment session

Hai bảng, cùng hình dạng — `exam_attempts.status` (0004) và `assessment_sessions.status` (0002):

```
in_progress  ──┬──▶  completed
               └──▶  abandoned   (bỏ dở, không tính vào bằng chứng có độ tin cao)
```

`abandoned` **không** bị xoá: bỏ dở giữa chừng cũng là một tín hiệu về độ khó và độ dài bài.

## 24. Learner experience state

`learner_experience_state.status` — migration 0028: chỉ hai giá trị, `completed` | `failed`.

**Không có `in_progress`** — có chủ đích: đang làm dở là trạng thái ở client, ghi vào D1 sẽ đẻ ra hàng loạt hàng rác mỗi lần learner đóng tab. `failed` = Học vượt (`skip`) sai câu nên trượt lượt đó.

Khoá: `(learner_id, subject_id, unit_key, exp_key)`. `exp_key` ∈ `skip` | `mid` | `final` | `explore:<node>` | `practice:<node>` | `practice<n>:<node>`.

***

## Phần E — Learner Intelligence

## 25. Model version

```
(mới)  ──▶  active  ──▶  superseded
```

Chỉ có **một** `active` cho mỗi `(learner_id, model_kind)`. Version mới chỉ sinh khi `content_hash` đổi — engine chạy ra kết quả y hệt thì **không** đẻ version, nhưng vẫn có dòng `engine_runs` (§27). Đã `superseded` thì bất biến vĩnh viễn; đó là điều làm cho việc "xem lại hôm đó hệ thống nghĩ gì" trở nên khả thi.

## 26. Goal

**Horizon** (theo số ngày còn lại):

```
≤ 14 ngày  → operational      ≤ 90 → tactical      > 90 → strategic      không deadline → undated
```

**Status**:

| Bảng | Giá trị | Ghi chú |
| --- | --- | --- |
| `learner_goals` | `active` | `achieved` | `dropped` | Goal mức tổng |
| `learner_goal_entries` | `active` | `achieved` | `dropped` | Từng mục tiêu do Goal Engine tái sinh từ `origin_kind`/`origin_id` |
| `learner_exam_targets` | `active` | `dropped` | **Không có `achieved`** — trường mục tiêu thì hoặc còn theo đuổi, hoặc bỏ; "đỗ rồi" là sự kiện ngoài hệ |

Goal biến mất khỏi Goal Model → `dropped` (**không xoá**, giữ lịch sử). Xuất hiện lại → `active`. Khoá idempotent `(learner_id, origin_kind, origin_id)` giữ cho engine chạy lại không đẻ trùng.

**Conflict flags**: `deadline_passed` · `time_competition`. Engine chỉ gắn cờ, **không tự giải quyết**.

## 27. Learner context event — lời khai bối cảnh

`learner_context_events.status` — migration 0030.

```
active  ──┬──▶  expired      (quá effective_to)
          └──▶  superseded   (lời khai mới cùng loại — superseded_by trỏ tới bản mới)
```

Lời khai mới **không ghi đè** lời cũ. Đây là lý do Planning Engine giải thích được "vì sao tuần trước kế hoạch nhẹ hơn": lời khai "con đang ốm" vẫn còn nguyên ở trạng thái `superseded`.

`kind` ∈ `time_budget`, `illness`, `exam_soon`, `busy`, `stress`, `motivation`, `focus_subject`, `other`. `source_role` ∈ `learner` | `parent`.

***

## Phần F — Vận hành

## 28. Workflow run

`workflow_runs.status` — migration 0027.

```
running  ──┬──▶  succeeded
           └──▶  failed
```

`workflow_run_steps.status` có thêm `skipped`: `succeeded` | `failed` | `skipped`. Một run có thể `failed` mà vẫn có step `succeeded` — [xử lý lỗi từng phần](workflows.md#xử-lý-lỗi).

Run kẹt ở `running` quá lâu = worker chết giữa chừng. Đó là tín hiệu vận hành thật, không phải giá trị rác — đừng dọn nó bằng cách ép về `failed` mà không ghi lý do.

## 29. Engine run

`engine_runs.status` — migration 0027: `running` → `succeeded` | `failed`.

Cột đi kèm mang nghĩa mà `status` không nói được:

| Cột | Nghĩa |
| --- | --- |
| `produced_change` | `0` = engine chạy xong, kết quả **không đổi** nên không sinh version mới. Đây là kết quả **thành công**, không phải hỏng. |
| `produced_model_kind` / `produced_version` | Version đã sinh, nối sang `learner_model_versions` (§25) |
| `workflow_run_id` | Engine chạy trong khuôn khổ workflow nào; NULL = chạy lẻ |

## 30. Queue dead letter

`queue_dead_letters.replay_status` — migration 0036. Cột **cho phép NULL**, và NULL có nghĩa riêng.

```
NULL (vừa rơi vào DLQ, chưa ai xử lý)
   │
   ├──▶ pending     (đã xếp hàng chờ replay)
   ├──▶ replayed    (đã chạy lại thành công — replayed_at được điền)
   ├──▶ failed      (chạy lại vẫn hỏng)
   └──▶ discarded   (quyết định bỏ — message không còn ý nghĩa, vd learner đã bị xoá)
```

Message vào đây sau `max_retries = 5` lần thất bại. `payload_json` giữ **nguyên văn** để replay đúng như cũ; `INSERT OR IGNORE` theo `(queue_name, message_id)` nên cùng một message chỉ nằm một dòng. Xem [queues](queues.md).

## 31. Rate limit bucket

`rate_limit_buckets` không có trạng thái — `(key, window_start, count)`. `window_start` đổi thì `count` reset về 1. Nêu ở đây để không ai đi tìm state machine của nó.

***

## Phần G — Tương tác & chân dung

## 32. Interaction & forum topic

| Bảng | Giá trị | Ghi chú |
| --- | --- | --- |
| `interactions` | `visible` → `hidden` → `blocked` | `blocked` là cuối; nội dung giữ lại để điều tra, không xoá |
| `forum_topics` | `open` → `hidden` | `locked` | `locked` = còn đọc được, không trả lời thêm |

Nội dung bị report vào hàng chờ kiểm duyệt (§17); media phải duyệt **trước khi** public (QG-008).

## 33. Portrait & nhánh liên quan

| Bảng | Giá trị |
| --- | --- |
| `portraits` | `active` → `archived` |
| `portrait_tracks` | `planned` → `active` → `paused` → `done` | `dropped` |
| `milestones` | `planned` → `in_progress` → `achieved` | `missed` | `rescheduled` |

`portrait_reactions` không có `status` — giá trị phản ứng là `agree` · `unsure` · `disagree`. Ba giá trị này **không** đổi hành vi Recommendation Engine: chúng là tiếng nói của đứa trẻ trong cuộc trò chuyện gia đình, không phải tín hiệu điều khiển hệ thống.

`portrait_learner_sections` cũng không có `status` — nội dung con viết chỉ có "đã viết" hoặc chưa; quyền ghi giới hạn ở role learner (AS-07.3.3).

## 34. Orca — thi đấu

| Bảng | Giá trị |
| --- | --- |
| `competitions` | `active` (mặc định) — **không có `CHECK`**, tập giá trị chưa chốt |
| `competition_editions` | `upcoming` (mặc định) — **không có `CHECK`** |

Ghi thẳng khoảng trống: hai cột này chưa có ràng buộc DB, nên bất kỳ chuỗi nào cũng ghi vào được. Cần thêm `CHECK` khi Orca ra khỏi giai đoạn thử.

***

## 35. Bắt buộc khi thêm trạng thái mới

1. `CHECK (status IN (...))` **trong migration** — không có CHECK thì cột không phải trạng thái, nó là ô ghi chú.
2. Thêm mục vào trang này với đủ: giá trị, mũi tên chuyển tiếp, **ai được đổi**, và nhánh nào là cuối.
3. Nói rõ giá trị mặc định và vì sao chọn giá trị đó (mặc định sai làm dữ liệu cũ đổi nghĩa im lặng).
4. Nếu có giá trị mang nghĩa "chưa ai xử lý" (NULL, `open`, `queued`) → nói rõ nó khác gì với "đã xử lý và kết luận là không làm gì".
5. Nghĩa nghiệp vụ của bảng đó → [data-semantics](data-semantics.md).

## Trace

* REQ-UX-03 (màu trạng thái), REQ-INT-28 (nhãn trí nhớ), REQ-INT-29 (model version), REQ-SEC-02, REQ-DOC-04.
* Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/index.md), [SDD-003](../architecture/sdd-003-knowledge-quality.md), [SDD-005](../architecture/sdd-005-interaction.md), [SDD-006](../architecture/sdd-006-reliability.md), [SDD-013](../architecture/sdd-013-coral-content-plane.md), [SDD-017](../architecture/sdd-017-retention.md).
* Trang anh em: [Data Semantics](data-semantics.md), [Data Dictionary](data-dictionary.md).
* Kiểm chứng: QG-004.
