The Learner Overlay
A build retrospective on the adaptive knowledge graph behind Amal, a kids' Arabic-learning app used by 95,000+ families. • Day 3 adds the second half of the system: a per-child layer on top of the shared graph that records what each child knows and predicts what they are about to forget.
Concept state: `user_concept_state`
The first overlay table, user_concept_state, answers "how well does this specific child know this specific concept?" There is one row per (child, concept) pair, and it carries a mastery strength — a number, roughly 0 to 1, for how solidly that child holds that concept, where a strength near 1 means "reliably known" and near 0 means "not yet".
-- one row per (child, concept): this child's grip on this node
SELECT user_id, concept_id, strength, sessions
FROM user_concept_state
WHERE user_id = 'child_A';
-- user_id | concept_id | strength | sessions
-- child_A | b_ltr | 0.91 | 6
-- child_A | b_snd | 0.88 | 5
-- child_A | baab | 0.40 | 2
This table is what lets the system ask questions a flat list never could: which concepts has this child mastered, which are still weak, which has she never started. Strength is the raw material for two later jobs — deciding what a child is ready for next (Day 4) and coloring the map parents see (Day 10). But strength alone has a blind spot: a concept you knew perfectly last month may have faded since. Handling that fade needs a second table.
Item memory and the forgetting problem
Knowing a concept once is not the same as knowing it now. Human memory fades on a predictable curve — you learn something, and unless you revisit it, the chance you can recall it drops as time passes. A learning system that ignores this will happily mark a child "done" with the letter ب and never bring it back, right up until the child has quietly forgotten it.
So the overlay needs to track, per practiced item, not just "did they get it right" but "how long will this memory last". That is the job of the second table, user_item_memory, and to use it you first need the memory model it is built on — HLR.
The diagram shows the curve every memory follows: right after practice, recall is near-certain; left alone, it decays. The moment it crosses 50% is the memory's half-life — and predicting that moment is exactly what HLR does.
THE FORGETTING CURVE (why "done" isn't forever)
recall
prob. 1.0 ┤●
│ ●
│ ●
0.5 ┤─────●─────────── <- "half-life": time to fall to 50%
│ ●
│ ● ● ● ● ●
0.0 ┼──────────────────────────► time since last practiceHLR from scratch: Half-Life Regression
HLR (Half-Life Regression) is a spaced-repetition memory model — popularized by Duolingo — that predicts how long a memory will last and uses that to schedule review at the best moment. Take it one word at a time, because the name is scarier than the idea:
- Half-life — the time until the chance of recalling an item drops to 50%. A freshly-learned item has a short half-life (you'll forget soon); an item you have recalled correctly many times has a long half-life (it sticks for weeks).
- Recall probability — given the half-life and how long it has been since the last practice, the estimated chance the child can recall the item right now. More time elapsed → lower probability.
- Regression — "regression" just means fitting a formula to data: HLR learns, from each child's actual right/wrong history, how much each correct answer should lengthen the half-life and each wrong answer should shorten it.
Put together, HLR says: every time a child successfully recalls an item, its half-life grows, so the next review can be scheduled further out; every time they miss it, the half-life shrinks and the item comes back sooner. Each item's memory row stores what HLR needs — the current half-life and when it was last practiced:
-- one row per (child, item): HLR's memory estimate
SELECT user_id, item_id, half_life_days, last_practiced
FROM user_item_memory
WHERE user_id = 'child_A';
-- user_id | item_id | half_life_days | last_practiced
-- child_A | b_ltr | 21.0 | 2026-07-02
-- child_A | b_snd | 9.0 | 2026-07-10
-- child_A | baab | 2.5 | 2026-07-15
Reading those rows the way HLR does: the letter ب has a 21-day half-life and was practiced two weeks ago, so its recall probability is still high — leave it alone. The word baab has only a 2.5-day half-life and was practiced yesterday, so it is about to slip — it should be scheduled for review soon. The system computes a recall probability per item and surfaces the ones that are decaying toward the forgetting point before they cross it.
Why the overlay is a separate layer
It is worth being explicit about why state and memory live in their own tables, apart from the graph, rather than being folded into the concept nodes. Keeping them separate is what keeps the system honest and cheap:
- The graph stays shared and small. Millions of per-child rows never touch
curriculum_concepts; the curriculum is authored once and read by everyone, while the overlay scales per child in its own tables. - One child's data never affects another's. Because each row is scoped to a
user_id, child A's progress and child B's progress are physically separate — a property that matters enormously for the privacy and per-child guarantees this system has to make. - The overlay is pure derived progress. It records what happened (strength, half-life, last practiced); it never redefines what the concepts or prerequisites are. That clean split is what lets Day 4 treat "the graph" and "this child's state" as two independent inputs to one function.
The diagram frames the whole design: the shared graph on one side, the per-child overlay on the other, deliberately kept apart so they can be combined cleanly. Day 4 is that combination — the pure function that takes both and returns the exact set of concepts a specific child is ready to learn next.
TWO INPUTS, KEPT APART
┌──────────────────┐ ┌──────────────────────────┐
│ CURRICULUM │ │ LEARNER OVERLAY (child) │
│ GRAPH (shared) │ │ user_concept_state │
│ concepts+edges │ │ user_item_memory (HLR) │
└──────────────────┘ └──────────────────────────┘
what to teach what THIS child knows
└───────────────┬───────────────┘
▼
combined on Day 4 →
"what is this child ready for?"Key takeaways
- The system is two layers: a shared curriculum graph (same for everyone) plus a per-child learner overlay (private progress), like a transparency sheet over a printed map.
user_concept_statestores one row per (child, concept) with a mastery strength (~0–1) — the raw signal for choosing what's next and coloring the parents' map.- Knowing a concept once isn't knowing it now: human memory decays on a forgetting curve, so the overlay must track how long each memory will last, not just past correctness.
- HLR (Half-Life Regression) predicts a memory's half-life (time until 50% recall); correct recalls lengthen it, misses shorten it, so review can be scheduled right before forgetting.
user_item_memorystores each item's half-life and last-practiced date; keeping the overlay in its own tables keeps the graph shared and small, isolates each child's data, and gives Day 4 two clean, independent inputs.
Checklist
- [ ] I can explain the graph-vs-overlay split using the map-and-transparency-sheet picture.
- [ ] I can say what
user_concept_statestores and what "mastery strength" means. - [ ] I can describe the forgetting curve and why "mastered once" isn't "known forever".
- [ ] I can define HLR, half-life, and recall probability in plain words, and say what lengthens or shortens a half-life.
- [ ] I can give two reasons the overlay is kept in separate tables from the graph.